diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml
index 90b78ed4c..62396bed1 100644
--- a/src/conversations/AndroidManifest.xml
+++ b/src/conversations/AndroidManifest.xml
@@ -20,6 +20,10 @@
android:name=".ui.MagicCreateActivity"
android:label="@string/create_new_account"
android:launchMode="singleTask" />
+
share());
+ if (bundle != null && bundle.containsKey("invite")) {
+ this.easyOnboardingInvite = bundle.getParcelable("invite");
+ if (this.easyOnboardingInvite != null) {
+ showInvite(this.easyOnboardingInvite);
+ return;
+ }
+ }
+ this.showLoading();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.easy_onboarding_invite, menu);
+ final MenuItem share = menu.findItem(R.id.action_share);
+ share.setVisible(easyOnboardingInvite != null);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if (menuItem.getItemId() == R.id.action_share) {
+ share();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(menuItem);
+ }
+ }
+
+ private void share() {
+ final String shareText = getString(
+ R.string.easy_invite_share_text,
+ easyOnboardingInvite.getDomain(),
+ easyOnboardingInvite.getLandingUrl()
+ );
+ final Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
+ sendIntent.setType("text/plain");
+ startActivity(Intent.createChooser(sendIntent, getString(R.string.share_invite_with)));
+ }
+
+ @Override
+ protected void refreshUiReal() {
+ invalidateOptionsMenu();
+ if (easyOnboardingInvite != null) {
+ showInvite(easyOnboardingInvite);
+ } else {
+ showLoading();
+ }
+ }
+
+ private void showLoading() {
+ this.binding.inProgress.setVisibility(View.VISIBLE);
+ this.binding.invite.setVisibility(View.GONE);
+ }
+
+ private void showInvite(final EasyOnboardingInvite invite) {
+ this.binding.inProgress.setVisibility(View.GONE);
+ this.binding.invite.setVisibility(View.VISIBLE);
+ this.binding.tapToShare.setText(getString(R.string.tap_share_button_send_invite, invite.getDomain()));
+ final Point size = new Point();
+ getWindowManager().getDefaultDisplay().getSize(size);
+ final int width = Math.min(size.x, size.y);
+ final String content;
+ if (Strings.isNullOrEmpty(invite.getLandingUrl())) {
+ content = invite.getUri();
+ } else {
+ content = invite.getLandingUrl();
+ }
+ final Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(content, width);
+ binding.qrCode.setImageBitmap(bitmap);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ if (easyOnboardingInvite != null) {
+ bundle.putParcelable("invite", easyOnboardingInvite);
+ }
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (easyOnboardingInvite != null) {
+ return;
+ }
+ final Intent launchIntent = getIntent();
+ final String accountExtra = launchIntent.getStringExtra(EXTRA_ACCOUNT);
+ final Jid jid = accountExtra == null ? null : Jid.ofEscaped(accountExtra);
+ if (jid == null) {
+ return;
+ }
+ final Account account = xmppConnectionService.findAccountByJid(jid);
+ xmppConnectionService.requestEasyOnboardingInvite(account, this);
+ }
+
+ public static void launch(final Account account, final Activity context) {
+ final Intent intent = new Intent(context, EasyOnboardingInviteActivity.class);
+ intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void inviteRequested(EasyOnboardingInvite invite) {
+ this.easyOnboardingInvite = invite;
+ Log.d(Config.LOGTAG, "invite requested");
+ refreshUi();
+ }
+
+ @Override
+ public void inviteRequestFailed(final String message) {
+ runOnUiThread(() -> {
+ if (!Strings.isNullOrEmpty(message)) {
+ Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+ }
+ finish();
+ });
+ }
+}
diff --git a/src/conversations/res/layout/activity_easy_invite.xml b/src/conversations/res/layout/activity_easy_invite.xml
new file mode 100644
index 000000000..8bbf11c03
--- /dev/null
+++ b/src/conversations/res/layout/activity_easy_invite.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/conversations/res/menu/easy_onboarding_invite.xml b/src/conversations/res/menu/easy_onboarding_invite.xml
new file mode 100644
index 000000000..0e086b515
--- /dev/null
+++ b/src/conversations/res/menu/easy_onboarding_invite.xml
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/src/conversations/res/values/strings.xml b/src/conversations/res/values/strings.xml
index a5e80e9f6..f9aaec9ee 100644
--- a/src/conversations/res/values/strings.xml
+++ b/src/conversations/res/values/strings.xml
@@ -9,4 +9,8 @@
You have been invited to %1$s. A username has already been picked for you. We will guide you through the process of creating an account.\nYou will be able to communicate with users of other providers by giving them your full XMPP address.
Your server invitation
Improperly formatted provisioning code
+ Tap the share button to send your contact an invitation to %1$s.
+ If your contact is nearby, they can also scan the code below to accept your invitation.
+ Join %1$s and chat with me: %2$s
+ Share invite with…
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 3225be67d..9adef0309 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -120,6 +120,7 @@ import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.EasyOnboardingInvite;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.PhoneHelper;
@@ -1619,6 +1620,43 @@ public class XmppConnectionService extends Service {
sendMessage(message, true, delay);
}
+ public void requestEasyOnboardingInvite(final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
+ final XmppConnection connection = account.getXmppConnection();
+ final Jid jid = connection == null ? null : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
+ if (jid == null) {
+ callback.inviteRequestFailed(getString(R.string.server_does_not_support_easy_onboarding_invites));
+ return;
+ }
+ final IqPacket request = new IqPacket(IqPacket.TYPE.SET);
+ request.setTo(jid);
+ final Element command = request.addChild("command", Namespace.COMMANDS);
+ command.setAttribute("node", Namespace.COMMANDS);
+ command.setAttribute("action", "execute");
+ sendIqPacket(account, request, (a, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ final Element resultCommand = response.findChild("command", Namespace.COMMANDS);
+ final Element x = resultCommand == null ? null : resultCommand.findChild("x", Namespace.DATA);
+ if (x != null) {
+ final Data data = Data.parse(x);
+ final String uri = data.getValue("uri");
+ final String landingUrl = data.getValue("landing-url");
+ if (uri != null) {
+ final EasyOnboardingInvite invite = new EasyOnboardingInvite(jid.getDomain().toEscapedString(), uri, landingUrl);
+ callback.inviteRequested(invite);
+ return;
+ }
+ }
+ callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
+ Log.d(Config.LOGTAG, response.toString());
+ } else if (response.getType() == IqPacket.TYPE.ERROR) {
+ callback.inviteRequestFailed(IqParser.extractErrorMessage(response));
+ } else {
+ callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
+ }
+ });
+
+ }
+
public void fetchRosterFromServer(final Account account) {
final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
if (!"".equals(account.getRosterVersion())) {
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index f0c3bc181..07e35a50e 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -2117,6 +2117,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
this.binding.textinput.setKeyboardListener(this);
messageListAdapter.updatePreferences();
refresh(false);
+ activity.invalidateOptionsMenu();
this.conversation.messagesLoaded.set(true);
Log.d(Config.LOGTAG, "scrolledToBottomAndNoPending=" + Boolean.toString(scrolledToBottomAndNoPending));
@@ -2397,7 +2398,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}
updateSendButton();
updateEditablity();
- activity.invalidateOptionsMenu();
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
index 049c0f27e..026a0cafa 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java
@@ -131,6 +131,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
@Override
protected void refreshUiReal() {
+ invalidateOptionsMenu();
for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
refreshFragment(id);
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java
index b76caaec7..ae81874e1 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java
@@ -30,6 +30,7 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
+import android.app.AlertDialog;
import android.app.Fragment;
import android.content.Intent;
import android.databinding.DataBindingUtil;
@@ -48,12 +49,16 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import com.google.common.collect.Collections2;
+
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding;
+import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
@@ -65,6 +70,7 @@ import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.ScrollState;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.AccountUtils;
+import eu.siacs.conversations.utils.EasyOnboardingInvite;
import eu.siacs.conversations.utils.ThemeHelper;
import static android.support.v7.widget.helper.ItemTouchHelper.LEFT;
@@ -300,6 +306,8 @@ public class ConversationsOverviewFragment extends XmppFragment {
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
AccountUtils.showHideMenuItems(menu);
+ final MenuItem easyOnboardInvite = menu.findItem(R.id.action_easy_invite);
+ easyOnboardInvite.setVisible(EasyOnboardingInvite.anyHasSupport(activity == null ? null : activity.xmppConnectionService));
}
@Override
@@ -354,10 +362,33 @@ public class ConversationsOverviewFragment extends XmppFragment {
case R.id.action_search:
startActivity(new Intent(getActivity(), SearchActivity.class));
return true;
+ case R.id.action_easy_invite:
+ selectAccountToStartEasyInvite();
+ return true;
}
return super.onOptionsItemSelected(item);
}
+ private void selectAccountToStartEasyInvite() {
+ final List accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
+ if (accounts.size() == 1) {
+ openEasyInviteScreen(accounts.get(0));
+ } else {
+ final AtomicReference selectedAccount = new AtomicReference<>(accounts.get(0));
+ final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity);
+ alertDialogBuilder.setTitle(R.string.choose_account);
+ final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toEscapedString()).toArray(new String[0]);
+ alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
+ alertDialogBuilder.setNegativeButton(R.string.cancel, null);
+ alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> openEasyInviteScreen(selectedAccount.get()));
+ alertDialogBuilder.create().show();
+ }
+ }
+
+ private void openEasyInviteScreen(final Account account) {
+ EasyOnboardingInviteActivity.launch(account, activity);
+ }
+
@Override
void refresh() {
if (this.binding == null || this.activity == null) {
diff --git a/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java
new file mode 100644
index 000000000..954e2c65a
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/utils/EasyOnboardingInvite.java
@@ -0,0 +1,94 @@
+package eu.siacs.conversations.utils;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collections;
+import java.util.List;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+public class EasyOnboardingInvite implements Parcelable {
+
+ private String domain;
+ private String uri;
+ private String landingUrl;
+
+ protected EasyOnboardingInvite(Parcel in) {
+ domain = in.readString();
+ uri = in.readString();
+ landingUrl = in.readString();
+ }
+
+ public EasyOnboardingInvite(String domain, String uri, String landingUrl) {
+ this.domain = domain;
+ this.uri = uri;
+ this.landingUrl = landingUrl;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(domain);
+ dest.writeString(uri);
+ dest.writeString(landingUrl);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public EasyOnboardingInvite createFromParcel(Parcel in) {
+ return new EasyOnboardingInvite(in);
+ }
+
+ @Override
+ public EasyOnboardingInvite[] newArray(int size) {
+ return new EasyOnboardingInvite[size];
+ }
+ };
+
+ public static boolean anyHasSupport(final XmppConnectionService service) {
+ if (QuickConversationsService.isQuicksy()) {
+ return false;
+ }
+ return getSupportingAccounts(service).size() > 0;
+
+ }
+
+ public static List getSupportingAccounts(final XmppConnectionService service) {
+ final ImmutableList.Builder supportingAccountsBuilder = new ImmutableList.Builder<>();
+ final List accounts = service == null ? Collections.emptyList() : service.getAccounts();
+ for(Account account : accounts) {
+ final XmppConnection xmppConnection = account.getXmppConnection();
+ if (xmppConnection != null && xmppConnection.getFeatures().easyOnboardingInvites()) {
+ supportingAccountsBuilder.add(account);
+ }
+ }
+ return supportingAccountsBuilder.build();
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public String getLandingUrl() {
+ return landingUrl;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public interface OnInviteRequested {
+ void inviteRequested(EasyOnboardingInvite invite);
+ void inviteRequestFailed(String message);
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java
index 31b3420dd..b65076016 100644
--- a/src/main/java/eu/siacs/conversations/xml/Namespace.java
+++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java
@@ -52,4 +52,5 @@ public final class Namespace {
public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat";
public static final String INVITE = "urn:xmpp:invite";
public static final String PARS = "urn:xmpp:pars:0";
+ public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 9929a9c81..3f699e64e 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -137,6 +137,7 @@ public class XmppConnection implements Runnable {
protected final Account account;
private final Features features = new Features(this);
private final HashMap disco = new HashMap<>();
+ private final HashMap commands = new HashMap<>();
private final SparseArray mStanzaQueue = new SparseArray<>();
private final Hashtable> packetCallbacks = new Hashtable<>();
private final Set advancedStreamFeaturesLoadedListeners = new HashSet<>();
@@ -228,6 +229,12 @@ public class XmppConnection implements Runnable {
}
}
+ public Jid getJidForCommand(final String node) {
+ synchronized (this.commands) {
+ return this.commands.get(node);
+ }
+ }
+
public void prepareNewConnection() {
this.lastConnect = SystemClock.elapsedRealtime();
this.lastPingSent = SystemClock.elapsedRealtime();
@@ -1028,6 +1035,9 @@ public class XmppConnection implements Runnable {
synchronized (this.disco) {
disco.clear();
}
+ synchronized (this.commands) {
+ this.commands.clear();
+ }
}
private void sendBindRequest() {
@@ -1250,6 +1260,35 @@ public class XmppConnection implements Runnable {
});
}
+ private void discoverCommands() {
+ final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
+ request.setTo(account.getDomain());
+ request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
+ sendIqPacket(request, (account, response) -> {
+ if (response.getType() == IqPacket.TYPE.RESULT) {
+ final Element query = response.findChild("query",Namespace.DISCO_ITEMS);
+ if (query == null) {
+ return;
+ }
+ final HashMap commands = new HashMap<>();
+ for(final Element child : query.getChildren()) {
+ if ("item".equals(child.getName())) {
+ final String node = child.getAttribute("node");
+ final Jid jid = child.getAttributeAsJid("jid");
+ if (node != null && jid != null) {
+ commands.put(node, jid);
+ }
+ }
+ }
+ Log.d(Config.LOGTAG,commands.toString());
+ synchronized (this.commands) {
+ this.commands.clear();
+ this.commands.putAll(commands);
+ }
+ }
+ });
+ }
+
public boolean isMamPreferenceAlways() {
return isMamPreferenceAlways;
}
@@ -1273,6 +1312,9 @@ public class XmppConnection implements Runnable {
if (getFeatures().carbons() && !features.carbonsEnabled) {
sendEnableCarbons();
}
+ if (getFeatures().commands()) {
+ discoverCommands();
+ }
}
private void sendServiceDiscoveryItems(final Jid server) {
@@ -1788,6 +1830,16 @@ public class XmppConnection implements Runnable {
return hasDiscoFeature(account.getDomain(), "urn:xmpp:carbons:2");
}
+ public boolean commands() {
+ return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
+ }
+
+ public boolean easyOnboardingInvites() {
+ synchronized (commands) {
+ return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE);
+ }
+ }
+
public boolean bookmarksConversion() {
return hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS_CONVERSION) && pepPublishOptions();
}
diff --git a/src/main/res/menu/fragment_conversations_overview.xml b/src/main/res/menu/fragment_conversations_overview.xml
index 52937ebd1..38c83df8f 100644
--- a/src/main/res/menu/fragment_conversations_overview.xml
+++ b/src/main/res/menu/fragment_conversations_overview.xml
@@ -35,6 +35,10 @@
android:title="@string/search_messages"
android:visible="@bool/show_individual_search_options"
app:showAsAction="never" />
+
- Failed deliveries
More options
No application found
+ Invite to Conversations
+ Unable to parse invite
+ Server does not support generating invites