diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml
index 90b78ed4c1b69df447fe8800f317edaa5bd4f8de..62396bed1af27fcdcac16db152115fbd362223ec 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 0000000000000000000000000000000000000000..8bbf11c037e00c299f152d5084093335d7f5f607
--- /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 0000000000000000000000000000000000000000..0e086b5150abfde6c0ee40430afc05582321e660
--- /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 a5e80e9f65177c4a3d71db3247855da12a9d8cca..f9aaec9ee76c1d13908cbde31e32755d139e29b9 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 3225be67d6ed63a0a1020f46b272ebe9d5424a1d..9adef030949f942120a5ca28bd1f28c0f854eb0b 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 f0c3bc181cd01803389a7166f35dedd2b89bf7d5..07e35a50eeb168b6fb7945fbbb30c67e1b0cca47 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 049c0f27ef1f827fe8cf4437ddfd2afae675dbdb..026a0cafa40b2daef9eb5cd074736738c765dfce 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 b76caaec7b48e7d6357d0bc4c74b69f28e450116..ae81874e1e300e01c5a8534dba1ee25cb73985b6 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 0000000000000000000000000000000000000000..954e2c65a27d35a294422bf024908b96ee2e2e98
--- /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 31b3420dd554015ba9bb2c2e078f14f222090ac3..b650760165d2bced466f18ab41772a9fb7803aae 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 9929a9c81496683b3c5e32a13ebaec284b1d9fd8..3f699e64ebf428e462539269d06afa4d8eefd49a 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 52937ebd16e51bf25e08ae3cc86dc6014455ad40..38c83df8fc601578f5a6370a65afa8a56eb83c07 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