add account provisioning via QR code to welcome screen

This commit is contained in:
Daniel Gultsch 2020-06-21 15:40:51 +02:00
parent 68960398b2
commit 15489547b7
9 changed files with 201 additions and 26 deletions

View file

@ -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,
}
}

View file

@ -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);

View file

@ -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<Jid> 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);
}
}

View file

@ -8,4 +8,5 @@
<string name="magic_create_text_on_x">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.</string>
<string name="magic_create_text_fixed">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.</string>
<string name="your_server_invitation">Your server invitation</string>
<string name="improperly_formatted_provisioning">Improperly formatted provisioning code</string>
</resources>

View file

@ -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 {

View file

@ -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<Jid> 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) == '}';
}
}

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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");
}
}