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 0000000000000000000000000000000000000000..702d45e2334ec518413c2eb0f170926d5497285a --- /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 142bda09a0e157a554f3dfc5babf2237865a2596..03e41a1bf5aea0513ede358f05b0a151addf53fa 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 0000000000000000000000000000000000000000..593291d95374f4be2f1722b8c46a03fe34ed28dc --- /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 77a2644cf7f3b426e937720883e8fad2399af056..a5e80e9f65177c4a3d71db3247855da12a9d8cca 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 48030e7f38b6b699c620eb1b9609669d189e5d20..812ca3716e6098ad13456e52690dbf4ea6c93755 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 ae63f493ad481698f1471869f59565ce535b0c7f..fdebd6a564991e8542a14b70c79b9644b472cc9b 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); - } + 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); } - Uri uri = Uri.parse(result); - handleUri(uri, 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 13e38e4878de7514bfb46587b7876a078ee1d466..b03ad5454b39b85dc9f938c0af88a4c2b308c952 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 9b27a123a98ba1f329ac557b940c114feafad8b5..5db19ad05f12bef5c1b4306171bd83a67a892bf2 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 0000000000000000000000000000000000000000..5b6cfae076da185b6e2af05b89ae39fe0e0d0f6a --- /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"); + } +}