diff --git a/src/conversations/java/eu/siacs/conversations/entities/AccountConfiguration.java b/src/conversations/java/eu/siacs/conversations/entities/AccountConfiguration.java new file mode 100644 index 000000000..702d45e23 --- /dev/null +++ b/src/conversations/java/eu/siacs/conversations/entities/AccountConfiguration.java @@ -0,0 +1,51 @@ +package eu.siacs.conversations.entities; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; + +import eu.siacs.conversations.xmpp.Jid; + +public class AccountConfiguration { + + private static final Gson GSON = new GsonBuilder().create(); + + public Protocol protocol; + public String address; + public String password; + + public Jid getJid() { + return Jid.ofEscaped(address); + } + + public static AccountConfiguration parse(final String input) { + final AccountConfiguration c; + try { + c = GSON.fromJson(input, AccountConfiguration.class); + } catch (JsonSyntaxException e) { + throw new IllegalArgumentException("Not a valid JSON string", e); + } + Preconditions.checkArgument( + c.protocol == Protocol.XMPP, + "Protocol must be XMPP" + ); + Preconditions.checkArgument( + c.address != null && c.getJid().isBareJid() && !c.getJid().isDomainJid(), + "Invalid XMPP address" + ); + Preconditions.checkArgument( + c.password != null && c.password.length() > 0, + "No password specified" + ); + return c; + } + + public enum Protocol { + @SerializedName("xmpp") XMPP, + } + +} + diff --git a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java index 142bda09a..03e41a1bf 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/WelcomeActivity.java @@ -26,6 +26,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityWelcomeBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.InstallReferrerUtils; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.XmppUri; @@ -61,12 +62,12 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi if (!xmppUri.isValidJid()) { return; } - final String preAuth = xmppUri.getParameter("preauth"); + final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); final Jid jid = xmppUri.getJid(); final Intent intent; if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth); - } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) { + } else if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); } else { @@ -146,10 +147,12 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.welcome_menu, menu); final MenuItem scan = menu.findItem(R.id.action_scan_qr_code); - scan.setVisible(getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)); + scan.setVisible(Compatibility.hasFeatureCamera(this)); return super.onCreateOptionsMenu(menu); } + + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -159,7 +162,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi } break; case R.id.action_scan_qr_code: - UriHandlerActivity.scan(this); + UriHandlerActivity.scan(this, true); break; case R.id.action_add_account_with_cert: addAccountFromKey(); @@ -186,7 +189,7 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi @Override public void onAccountCreated(final Account account) { final Intent intent = new Intent(this, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().asBareJid().toString()); + intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); intent.putExtra("init", true); addInviteUri(intent); startActivity(intent); diff --git a/src/conversations/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/conversations/java/eu/siacs/conversations/utils/ProvisioningUtils.java new file mode 100644 index 000000000..593291d95 --- /dev/null +++ b/src/conversations/java/eu/siacs/conversations/utils/ProvisioningUtils.java @@ -0,0 +1,43 @@ +package eu.siacs.conversations.utils; + +import android.app.Activity; +import android.content.Intent; +import android.widget.Toast; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.AccountConfiguration; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditAccountActivity; +import eu.siacs.conversations.xmpp.Jid; + +public class ProvisioningUtils { + + public static void provision(final Activity activity, final String json) { + final AccountConfiguration accountConfiguration; + try { + accountConfiguration = AccountConfiguration.parse(json); + } catch (final IllegalArgumentException e) { + Toast.makeText(activity, R.string.improperly_formatted_provisioning, Toast.LENGTH_LONG).show(); + return; + } + final Jid jid = accountConfiguration.getJid(); + final List accounts = DatabaseBackend.getInstance(activity).getAccountJids(true); + if (accounts.contains(jid)) { + Toast.makeText(activity, R.string.account_already_exists, Toast.LENGTH_LONG).show(); + return; + } + final Intent serviceIntent = new Intent(activity, XmppConnectionService.class); + serviceIntent.setAction(XmppConnectionService.ACTION_PROVISION_ACCOUNT); + serviceIntent.putExtra("address", jid.asBareJid().toEscapedString()); + serviceIntent.putExtra("password", accountConfiguration.password); + Compatibility.startService(activity, serviceIntent); + final Intent intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", jid.asBareJid().toEscapedString()); + intent.putExtra("init", true); + activity.startActivity(intent); + } + +} diff --git a/src/conversations/res/values/strings.xml b/src/conversations/res/values/strings.xml index 77a2644cf..a5e80e9f6 100644 --- a/src/conversations/res/values/strings.xml +++ b/src/conversations/res/values/strings.xml @@ -8,4 +8,5 @@ You have been invited to %1$s. We will guide you through the process of creating an account.\nWhen picking %1$s as a provider you will be able to communicate with users of other providers by giving them your full XMPP address. 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 \ 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 48030e7f3..812ca3716 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -170,6 +170,7 @@ public class XmppConnectionService extends Service { public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received"; public static final String ACTION_DISMISS_CALL = "dismiss_call"; public static final String ACTION_END_CALL = "end_call"; + public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; @@ -659,6 +660,15 @@ public class XmppConnectionService extends Service { mJingleConnectionManager.endRtpSession(sessionId); } break; + case ACTION_PROVISION_ACCOUNT: { + final String address = intent.getStringExtra("address"); + final String password = intent.getStringExtra("password"); + if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) { + break; + } + provisionAccount(address, password); + break; + } case ACTION_DISMISS_ERROR_NOTIFICATIONS: dismissErrorNotifications(); break; @@ -2180,6 +2190,14 @@ public class XmppConnectionService extends Service { } } + private void provisionAccount(final String address, final String password) { + final Jid jid = Jid.ofEscaped(address); + final Account account = new Account(jid, password); + account.setOption(Account.OPTION_DISABLED, true); + Log.d(Config.LOGTAG,jid.asBareJid().toEscapedString()+": provisioning account"); + createAccount(account); + } + public void createAccountFromKey(final String alias, final OnAccountCreated callback) { new Thread(() -> { try { diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index ae63f493a..fdebd6a56 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -11,12 +11,16 @@ import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.widget.Toast; +import com.google.common.base.Strings; + import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import eu.siacs.conversations.R; import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.utils.ProvisioningUtils; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; @@ -24,29 +28,45 @@ import eu.siacs.conversations.xmpp.Jid; public class UriHandlerActivity extends AppCompatActivity { public static final String ACTION_SCAN_QR_CODE = "scan_qr_code"; + private static final String EXTRA_ALLOW_PROVISIONING = "extra_allow_provisioning"; private static final int REQUEST_SCAN_QR_CODE = 0x1234; private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN = 0x6789; - private static final Pattern VCARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n"); + private static final int REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION = 0x6790; + private static final Pattern V_CARD_XMPP_PATTERN = Pattern.compile("\nIMPP([^:]*):(xmpp:.+)\n"); private boolean handled = false; - public static void scan(Activity activity) { + public static void scan(final Activity activity) { + scan(activity, false); + } + + public static void scan(final Activity activity, final boolean provisioning) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { - Intent intent = new Intent(activity, UriHandlerActivity.class); + final Intent intent = new Intent(activity, UriHandlerActivity.class); intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); + if (provisioning) { + intent.putExtra(EXTRA_ALLOW_PROVISIONING, true); + } intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); activity.startActivity(intent); } else { - activity.requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSIONS_TO_SCAN); + activity.requestPermissions( + new String[]{Manifest.permission.CAMERA}, + provisioning ? REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION : REQUEST_CAMERA_PERMISSIONS_TO_SCAN + ); } } public static void onRequestPermissionResult(Activity activity, int requestCode, int[] grantResults) { - if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN) { + if (requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN && requestCode != REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) { return; } if (grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - scan(activity); + if (requestCode == REQUEST_CAMERA_PERMISSIONS_TO_SCAN_AND_PROVISION) { + scan(activity, true); + } else { + scan(activity); + } } else { Toast.makeText(activity, R.string.qr_code_scanner_needs_access_to_camera, Toast.LENGTH_SHORT).show(); } @@ -88,19 +108,19 @@ public class UriHandlerActivity extends AppCompatActivity { final List accounts = DatabaseBackend.getInstance(this).getAccountJids(true); if (SignupUtils.isSupportTokenRegistry() && xmppUri.isValidJid()) { - final String preauth = xmppUri.getParameter("preauth"); + final String preAuth = xmppUri.getParameter(XmppUri.PARAMETER_PRE_AUTH); final Jid jid = xmppUri.getJid(); if (xmppUri.isAction(XmppUri.ACTION_REGISTER)) { if (jid.getEscapedLocal() != null && accounts.contains(jid.asBareJid())) { Toast.makeText(this, R.string.account_already_exists, Toast.LENGTH_LONG).show(); return; } - intent = SignupUtils.getTokenRegistrationIntent(this, jid, preauth); + intent = SignupUtils.getTokenRegistrationIntent(this, jid, preAuth); startActivity(intent); return; } - if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter("ibr"))) { - intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preauth); + if (xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y".equals(xmppUri.getParameter(XmppUri.PARAMETER_IBR))) { + intent = SignupUtils.getTokenRegistrationIntent(this, jid.getDomain(), preAuth); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); startActivity(intent); return; @@ -194,22 +214,38 @@ public class UriHandlerActivity extends AppCompatActivity { finish(); } + private boolean allowProvisioning() { + final Intent launchIntent = getIntent(); + return launchIntent != null && launchIntent.getBooleanExtra(EXTRA_ALLOW_PROVISIONING, false); + } + @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { super.onActivityResult(requestCode, requestCode, intent); if (requestCode == REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { - String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); - if (result != null) { - if (result.startsWith("BEGIN:VCARD\n")) { - Matcher matcher = VCARD_XMPP_PATTERN.matcher(result); - if (matcher.find()) { - result = matcher.group(2); - } - } - Uri uri = Uri.parse(result); - handleUri(uri, true); + final String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); + if (Strings.isNullOrEmpty(result)) { + finish(); + return; } + if (result.startsWith("BEGIN:VCARD\n")) { + final Matcher matcher = V_CARD_XMPP_PATTERN.matcher(result); + if (matcher.find()) { + handleUri(Uri.parse(matcher.group(2)), true); + } + finish(); + return; + } else if (QuickConversationsService.isConversations() && looksLikeJsonObject(result) && allowProvisioning()) { + ProvisioningUtils.provision(this, result); + finish(); + return; + } + handleUri(Uri.parse(result), true); } finish(); } + + private static boolean looksLikeJsonObject(final String input) { + return input.charAt(0) == '{' && input.charAt(input.length() - 1) == '}'; + } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/Compatibility.java b/src/main/java/eu/siacs/conversations/utils/Compatibility.java index 13e38e487..b03ad5454 100644 --- a/src/main/java/eu/siacs/conversations/utils/Compatibility.java +++ b/src/main/java/eu/siacs/conversations/utils/Compatibility.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.utils; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -139,4 +140,15 @@ public class Compatibility { Log.d(Config.LOGTAG, context.getClass().getSimpleName() + " was unable to start service"); } } + + + @SuppressLint("UnsupportedChromeOsCameraSystemFeature") + public static boolean hasFeatureCamera(final Context context) { + final PackageManager packageManager = context.getPackageManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } else { + return packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 9b27a123a..5db19ad05 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -22,6 +22,8 @@ public class XmppUri { public static final String ACTION_MESSAGE = "message"; public static final String ACTION_REGISTER = "register"; public static final String ACTION_ROSTER = "roster"; + public static final String PARAMETER_PRE_AUTH = "preauth"; + public static final String PARAMETER_IBR = "ibr"; private static final String OMEMO_URI_PARAM = "omemo-sid-"; protected Uri uri; protected String jid; diff --git a/src/quicksy/java/eu/siacs/conversations/utils/ProvisioningUtils.java b/src/quicksy/java/eu/siacs/conversations/utils/ProvisioningUtils.java new file mode 100644 index 000000000..5b6cfae07 --- /dev/null +++ b/src/quicksy/java/eu/siacs/conversations/utils/ProvisioningUtils.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.utils; + +import eu.siacs.conversations.ui.UriHandlerActivity; + +public class ProvisioningUtils { + public static void provision(UriHandlerActivity uriHandlerActivity, String result) { + throw new IllegalStateException("Quicksy does not support provisioning"); + } +}