Detailed changes
@@ -188,16 +188,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq generateSetPassword(final Account account, final String newPassword) {
- final Iq packet = new Iq(Iq.Type.SET);
- packet.setTo(account.getDomain());
- final Element query = packet.addChild("query", Namespace.REGISTER);
- final Jid jid = account.getJid();
- query.addChild("username").setContent(jid.getLocal());
- query.addChild("password").setContent(newPassword);
- return packet;
- }
-
public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) {
List<Jid> jids = new ArrayList<>();
jids.add(jid);
@@ -39,7 +39,6 @@ import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.security.KeyChain;
import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.util.LruCache;
import android.util.Pair;
@@ -141,6 +140,7 @@ import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManage
import eu.siacs.conversations.xmpp.manager.NickManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
import eu.siacs.conversations.xmpp.manager.PrivateStorageManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import eu.siacs.conversations.xmpp.manager.RosterManager;
import eu.siacs.conversations.xmpp.manager.VCardManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
@@ -2809,41 +2809,10 @@ public class XmppConnectionService extends Service {
}
}
- public void updateAccountPasswordOnServer(
- final Account account,
- final String newPassword,
- final OnAccountPasswordChanged callback) {
- final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
- sendIqPacket(
- account,
- iq,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
- account.setPassword(newPassword);
- account.setOption(Account.OPTION_MAGIC_CREATE, false);
- databaseBackend.updateAccount(account);
- callback.onPasswordChangeSucceeded();
- } else {
- callback.onPasswordChangeFailed();
- }
- });
- }
-
- public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
- final Iq iqPacket = new Iq(Iq.Type.SET);
- final Element query = iqPacket.addChild("query", Namespace.REGISTER);
- query.addChild("remove");
- sendIqPacket(
- account,
- iqPacket,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- deleteAccount(account);
- callback.accept(true);
- } else {
- callback.accept(false);
- }
- });
+ public ListenableFuture<Void> updateAccountPasswordOnServer(
+ final Account account, final String newPassword) {
+ final var connection = account.getXmppConnection();
+ return connection.getManager(RegistrationManager.class).setPassword(newPassword);
}
public void deleteAccount(final Account account) {
@@ -4664,21 +4633,24 @@ public class XmppConnectionService extends Service {
}
}
- public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
- if (mOnCaptchaRequested.size() > 0) {
- DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
- Bitmap scaled =
- Bitmap.createScaledBitmap(
- captcha,
- (int) (captcha.getWidth() * metrics.scaledDensity),
- (int) (captcha.getHeight() * metrics.scaledDensity),
- false);
- for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
- listener.onCaptchaRequested(account, id, data, scaled);
- }
- return true;
+ public boolean displayCaptchaRequest(
+ final Account account,
+ final im.conversations.android.xmpp.model.data.Data data,
+ final Bitmap captcha) {
+ if (mOnCaptchaRequested.isEmpty()) {
+ return false;
}
- return false;
+ final var metrics = getApplicationContext().getResources().getDisplayMetrics();
+ Bitmap scaled =
+ Bitmap.createScaledBitmap(
+ captcha,
+ (int) (captcha.getWidth() * metrics.scaledDensity),
+ (int) (captcha.getHeight() * metrics.scaledDensity),
+ false);
+ for (final OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
+ listener.onCaptchaRequested(account, data, scaled);
+ }
+ return true;
}
public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
@@ -5027,14 +4999,6 @@ public class XmppConnectionService extends Service {
}
}
- public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
- final XmppConnection connection = account.getXmppConnection();
- if (connection == null) {
- return;
- }
- connection.sendCreateAccountWithCaptchaPacket(id, data);
- }
-
public ListenableFuture<Iq> sendIqPacket(final Account account, final Iq request) {
final XmppConnection connection = account.getXmppConnection();
if (connection == null) {
@@ -5401,12 +5365,6 @@ public class XmppConnectionService extends Service {
void informUser(int r);
}
- public interface OnAccountPasswordChanged {
- void onPasswordChangeSucceeded();
-
- void onPasswordChangeFailed();
- }
-
public interface OnRoomDestroy {
void onRoomDestroySucceeded();
@@ -5440,7 +5398,10 @@ public class XmppConnectionService extends Service {
}
public interface OnCaptchaRequested {
- void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
+ void onCaptchaRequested(
+ Account account,
+ im.conversations.android.xmpp.model.data.Data data,
+ Bitmap captcha);
}
public interface OnRosterUpdate {
@@ -2,119 +2,130 @@ package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
import android.view.View;
import android.widget.Toast;
-
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
import androidx.databinding.DataBindingUtil;
-
import com.google.android.material.textfield.TextInputLayout;
-
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityChangePasswordBinding;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.widget.DisabledActionModeCallback;
-public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged {
-
- private ActivityChangePasswordBinding binding;
-
- private final View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() {
- @Override
- public void onClick(final View view) {
- final var account = mAccount;
- if (account == null) {
- return;
- }
- final String currentPassword = binding.currentPassword.getText().toString();
- final String newPassword = binding.newPassword.getText().toString();
- if (!account.isOptionSet(Account.OPTION_MAGIC_CREATE) && !currentPassword.equals(account.getPassword())) {
- binding.currentPassword.requestFocus();
- binding.currentPasswordLayout.setError(getString(R.string.account_status_unauthorized));
- removeErrorsOnAllBut(binding.currentPasswordLayout);
- } else if (newPassword.trim().isEmpty()) {
- binding.newPassword.requestFocus();
- binding.newPasswordLayout.setError(getString(R.string.password_should_not_be_empty));
- removeErrorsOnAllBut(binding.newPasswordLayout);
- } else {
- binding.currentPasswordLayout.setError(null);
- binding.newPasswordLayout.setError(null);
- xmppConnectionService.updateAccountPasswordOnServer(account, newPassword, ChangePasswordActivity.this);
- binding.changePasswordButton.setEnabled(false);
- binding.changePasswordButton.setText(R.string.updating);
- }
- }
- };
-
-
-
- private Account mAccount;
-
- @Override
+public class ChangePasswordActivity extends XmppActivity {
+
+ private ActivityChangePasswordBinding binding;
+
+ private final FutureCallback<? super Void> passwordChangedCallback =
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ Toast.makeText(
+ ChangePasswordActivity.this,
+ R.string.password_changed,
+ Toast.LENGTH_LONG)
+ .show();
+ finish();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not change password", t);
+ binding.newPasswordLayout.setError(
+ getString(R.string.could_not_change_password));
+ binding.changePasswordButton.setEnabled(true);
+ binding.changePasswordButton.setText(R.string.change_password);
+ }
+ };
+ private final View.OnClickListener mOnChangePasswordButtonClicked =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(final View view) {
+ final var account = mAccount;
+ if (account == null) {
+ return;
+ }
+ final String currentPassword = binding.currentPassword.getText().toString();
+ final String newPassword = binding.newPassword.getText().toString();
+ if (!account.isOptionSet(Account.OPTION_MAGIC_CREATE)
+ && !currentPassword.equals(account.getPassword())) {
+ binding.currentPassword.requestFocus();
+ binding.currentPasswordLayout.setError(
+ getString(R.string.account_status_unauthorized));
+ removeErrorsOnAllBut(binding.currentPasswordLayout);
+ } else if (newPassword.trim().isEmpty()) {
+ binding.newPassword.requestFocus();
+ binding.newPasswordLayout.setError(
+ getString(R.string.password_should_not_be_empty));
+ removeErrorsOnAllBut(binding.newPasswordLayout);
+ } else {
+ binding.currentPasswordLayout.setError(null);
+ binding.newPasswordLayout.setError(null);
+ final var future =
+ xmppConnectionService.updateAccountPasswordOnServer(
+ account, newPassword);
+ Futures.addCallback(
+ future,
+ ChangePasswordActivity.this.passwordChangedCallback,
+ ContextCompat.getMainExecutor(getApplication()));
+ binding.changePasswordButton.setEnabled(false);
+ binding.changePasswordButton.setText(R.string.updating);
+ }
+ }
+ };
+
+ private Account mAccount;
+
+ @Override
protected void onBackendConnected() {
- this.mAccount = extractAccount(getIntent());
- if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
- this.binding.currentPasswordLayout.setVisibility(View.GONE);
- } else {
- this.binding.currentPasswordLayout.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password);
- Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
- setSupportActionBar(binding.toolbar);
- configureActionBar(getSupportActionBar());
- binding.cancelButton.setOnClickListener(view -> finish());
- binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
- binding.currentPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
- binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
- }
-
- @Override
- public void onStart() {
- super.onStart();
- Intent intent = getIntent();
- String password = intent != null ? intent.getStringExtra("password") : null;
- if (password != null) {
- binding.newPassword.getEditableText().clear();
- binding.newPassword.getEditableText().append(password);
- }
- }
-
- @Override
- public void onPasswordChangeSucceeded() {
- runOnUiThread(() -> {
- Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show();
- finish();
- });
- }
-
- @Override
- public void onPasswordChangeFailed() {
- runOnUiThread(() -> {
- binding.newPasswordLayout.setError(getString(R.string.could_not_change_password));
- binding.changePasswordButton.setEnabled(true);
- binding.changePasswordButton.setText(R.string.change_password);
- });
-
- }
-
- private void removeErrorsOnAllBut(TextInputLayout exception) {
- if (this.binding.currentPasswordLayout != exception) {
- this.binding.currentPasswordLayout.setErrorEnabled(false);
- this.binding.currentPasswordLayout.setError(null);
- }
- if (this.binding.newPasswordLayout != exception) {
- this.binding.newPasswordLayout.setErrorEnabled(false);
- this.binding.newPasswordLayout.setError(null);
- }
-
- }
-
- public void refreshUiReal() {
-
- }
+ this.mAccount = extractAccount(getIntent());
+ if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
+ this.binding.currentPasswordLayout.setVisibility(View.GONE);
+ } else {
+ this.binding.currentPasswordLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ this.binding = DataBindingUtil.setContentView(this, R.layout.activity_change_password);
+ Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+ configureActionBar(getSupportActionBar());
+ binding.cancelButton.setOnClickListener(view -> finish());
+ binding.changePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
+ binding.currentPassword.setCustomSelectionActionModeCallback(
+ new DisabledActionModeCallback());
+ binding.newPassword.setCustomSelectionActionModeCallback(new DisabledActionModeCallback());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Intent intent = getIntent();
+ String password = intent != null ? intent.getStringExtra("password") : null;
+ if (password != null) {
+ binding.newPassword.getEditableText().clear();
+ binding.newPassword.getEditableText().append(password);
+ }
+ }
+
+ private void removeErrorsOnAllBut(TextInputLayout exception) {
+ if (this.binding.currentPasswordLayout != exception) {
+ this.binding.currentPasswordLayout.setErrorEnabled(false);
+ this.binding.currentPasswordLayout.setError(null);
+ }
+ if (this.binding.newPasswordLayout != exception) {
+ this.binding.newPasswordLayout.setErrorEnabled(false);
+ this.binding.newPasswordLayout.setError(null);
+ }
+ }
+
+ public void refreshUiReal() {}
}
@@ -80,8 +80,9 @@ import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
-import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
+import im.conversations.android.xmpp.model.data.Data;
import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Arrays;
import java.util.List;
@@ -788,10 +789,10 @@ public class EditAccountActivity extends OmemoActivity
showBlocklist.setVisible(false);
}
- if (!mAccount.getXmppConnection().getFeatures().register()) {
- changePassword.setVisible(false);
- deleteAccount.setVisible(false);
- }
+ final var registration =
+ mAccount.getXmppConnection().getManager(RegistrationManager.class).hasFeature();
+ changePassword.setVisible(registration);
+ deleteAccount.setVisible(registration);
mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
changePresence.setVisible(!mInitMode);
} else {
@@ -1601,8 +1602,7 @@ public class EditAccountActivity extends OmemoActivity
}
@Override
- public void onCaptchaRequested(
- final Account account, final String id, final Data data, final Bitmap captcha) {
+ public void onCaptchaRequested(final Account account, final Data data, final Bitmap captcha) {
runOnUiThread(
() -> {
if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) {
@@ -1624,34 +1624,15 @@ public class EditAccountActivity extends OmemoActivity
builder.setPositiveButton(
getString(R.string.ok),
- (dialog, which) -> {
- String rc = input.getText().toString();
- data.put("username", account.getUsername());
- data.put("password", account.getPassword());
- data.put("ocr", rc);
- data.submit();
-
- if (xmppConnectionServiceBound) {
- xmppConnectionService.sendCreateAccountWithCaptchaPacket(
- account, id, data);
- }
- });
+ (dialog, which) ->
+ account.getXmppConnection()
+ .register(data, input.getText().toString()));
builder.setNegativeButton(
getString(R.string.cancel),
- (dialog, which) -> {
- if (xmppConnectionService != null) {
- xmppConnectionService.sendCreateAccountWithCaptchaPacket(
- account, null, null);
- }
- });
+ (dialog, which) -> account.getXmppConnection().cancelRegistration());
builder.setOnCancelListener(
- dialog -> {
- if (xmppConnectionService != null) {
- xmppConnectionService.sendCreateAccountWithCaptchaPacket(
- account, null, null);
- }
- });
+ dialog -> account.getXmppConnection().cancelRegistration());
mCaptchaDialog = builder.create();
mCaptchaDialog.show();
input.requestFocus();
@@ -56,6 +56,9 @@ import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
@@ -86,6 +89,7 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -390,92 +394,96 @@ public abstract class XmppActivity extends ActionBarActivity {
protected void deleteAccount(final Account account, final Runnable postDelete) {
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
- final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server);
builder.setView(dialogView);
builder.setTitle(R.string.mgmt_account_delete);
builder.setPositiveButton(getString(R.string.delete), null);
builder.setNegativeButton(getString(R.string.cancel), null);
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(
- dialogInterface -> {
- final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
- button.setOnClickListener(
- v -> {
- final boolean unregister = deleteFromServer.isChecked();
- if (unregister) {
- if (account.isOnlineAndConnected()) {
- deleteFromServer.setEnabled(false);
- button.setText(R.string.please_wait);
- button.setEnabled(false);
- xmppConnectionService.unregisterAccount(
- account,
- result -> {
- runOnUiThread(
- () -> {
- if (result) {
- dialog.dismiss();
- if (postDelete != null) {
- postDelete.run();
- }
- if (xmppConnectionService
- .getAccounts()
- .size()
- == 0
- && Config
- .MAGIC_CREATE_DOMAIN
- != null) {
- final Intent intent =
- SignupUtils
- .getSignUpIntent(
- this);
- intent.setFlags(
- Intent
- .FLAG_ACTIVITY_NEW_TASK
- | Intent
- .FLAG_ACTIVITY_CLEAR_TASK);
- startActivity(intent);
- }
- } else {
- deleteFromServer.setEnabled(
- true);
- button.setText(R.string.delete);
- button.setEnabled(true);
- Toast.makeText(
- this,
- R.string
- .could_not_delete_account_from_server,
- Toast
- .LENGTH_LONG)
- .show();
- }
- });
- });
- } else {
- Toast.makeText(
- this,
- R.string.not_connected_try_again,
- Toast.LENGTH_LONG)
- .show();
- }
- } else {
- xmppConnectionService.deleteAccount(account);
- dialog.dismiss();
- if (xmppConnectionService.getAccounts().size() == 0
- && Config.MAGIC_CREATE_DOMAIN != null) {
- final Intent intent = SignupUtils.getSignUpIntent(this);
- intent.setFlags(
- Intent.FLAG_ACTIVITY_NEW_TASK
- | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- startActivity(intent);
- } else if (postDelete != null) {
- postDelete.run();
- }
- }
- });
- });
+ dialogInterface -> onShowDeleteDialog(dialogInterface, account, postDelete));
dialog.show();
}
+ private void onShowDeleteDialog(
+ final DialogInterface dialogInterface,
+ final Account account,
+ final Runnable postDelete) {
+ final AlertDialog alertDialog;
+ if (dialogInterface instanceof AlertDialog dialog) {
+ alertDialog = dialog;
+ } else {
+ throw new IllegalStateException("DialogInterface was not of type AlertDialog");
+ }
+ final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ button.setOnClickListener(
+ v -> onDeleteDialogButtonClicked(alertDialog, account, postDelete));
+ }
+
+ private void onDeleteDialogButtonClicked(
+ final AlertDialog dialog, final Account account, final Runnable postDelete) {
+ final CheckBox deleteFromServer = dialog.findViewById(R.id.delete_from_server);
+ if (deleteFromServer == null) {
+ throw new IllegalStateException("AlertDialog did not have button");
+ }
+ final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ final boolean unregister = deleteFromServer.isChecked();
+ if (unregister) {
+ if (account.isOnlineAndConnected()) {
+ final var connection = account.getXmppConnection();
+ deleteFromServer.setEnabled(false);
+ button.setText(R.string.please_wait);
+ button.setEnabled(false);
+ final var future = connection.getManager(RegistrationManager.class).unregister();
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ runOnUiThread(
+ () -> onAccountDeletedSuccess(account, dialog, postDelete));
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "could not unregister account", t);
+ runOnUiThread(() -> onAccountDeletionFailure(dialog, postDelete));
+ }
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ onAccountDeletedSuccess(account, dialog, postDelete);
+ }
+ }
+
+ private void onAccountDeletedSuccess(
+ final Account account, final AlertDialog dialog, final Runnable postDelete) {
+ xmppConnectionService.deleteAccount(account);
+ dialog.dismiss();
+ if (xmppConnectionService.getAccounts().isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) {
+ final Intent intent = SignupUtils.getSignUpIntent(this);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ startActivity(intent);
+ } else if (postDelete != null) {
+ postDelete.run();
+ }
+ }
+
+ private void onAccountDeletionFailure(final AlertDialog dialog, final Runnable postDelete) {
+ final CheckBox deleteFromServer = dialog.findViewById(R.id.delete_from_server);
+ if (deleteFromServer == null) {
+ throw new IllegalStateException("AlertDialog did not have button");
+ }
+ final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
+ deleteFromServer.setEnabled(true);
+ button.setText(R.string.delete);
+ button.setEnabled(true);
+ Toast.makeText(this, R.string.could_not_delete_account_from_server, Toast.LENGTH_LONG)
+ .show();
+ }
+
protected abstract void onBackendConnected();
protected void registerListeners() {
@@ -3,6 +3,7 @@ package eu.siacs.conversations.xml;
public final class Namespace {
public static final String ADDRESSING = "http://jabber.org/protocol/address";
public static final String AXOLOTL = "eu.siacs.conversations.axolotl";
+ public static final String BOB = "urn:xmpp:bob";
public static final String PGP_SIGNED = "jabber:x:signed";
public static final String PGP_ENCRYPTED = "jabber:x:encrypted";
public static final String AXOLOTL_BUNDLES = AXOLOTL + ".bundles";
@@ -98,7 +99,7 @@ public final class Namespace {
public static final String MUC_USER = "http://jabber.org/protocol/muc#user";
public static final String BOOKMARKS2 = "urn:xmpp:bookmarks:1";
public static final String BOOKMARKS2_COMPAT = BOOKMARKS2 + "#compat";
- public static final String INVITE = "urn:xmpp:invite";
+ public static final String PRE_AUTHENTICATED_IN_BAND_REGISTRATION = "urn:xmpp:ibr-token:0";
public static final String PARS = "urn:xmpp:pars:0";
public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
public static final String OMEMO_DTLS_SRTP_VERIFICATION =
@@ -110,6 +111,7 @@ public final class Namespace {
public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam";
public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264";
public static final String HASHES = "urn:xmpp:hashes:2";
+ public static final String MEDIA_ELEMENT = "urn:xmpp:media-element";
public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
@@ -3,8 +3,6 @@ package eu.siacs.conversations.xmpp;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.SystemClock;
import android.security.KeyChain;
@@ -44,8 +42,6 @@ import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramMechanism;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.generator.IqGenerator;
-import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser;
@@ -70,13 +66,13 @@ import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter;
import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.bind.Bind2;
-import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import eu.siacs.conversations.xmpp.manager.AbstractManager;
import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
import eu.siacs.conversations.xmpp.manager.PingManager;
+import eu.siacs.conversations.xmpp.manager.RegistrationManager;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.IqErrorException;
import im.conversations.android.xmpp.model.AuthenticationFailure;
@@ -120,9 +116,7 @@ import im.conversations.android.xmpp.model.tls.StartTls;
import im.conversations.android.xmpp.processor.AccountStateProcessor;
import im.conversations.android.xmpp.processor.BindProcessor;
import im.conversations.android.xmpp.processor.MessageAcknowledgedProcessor;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.net.ConnectException;
import java.net.IDN;
import java.net.InetAddress;
@@ -135,7 +129,6 @@ import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -260,9 +253,13 @@ public class XmppConnection implements Runnable {
}
}
- private void changeStatus(final Account.State nextStatus) {
+ private void changeState(final Account.State nextStatus) {
+ this.changeState(nextStatus, true);
+ }
+
+ private void changeState(final Account.State nextStatus, final boolean skipOnInterrupt) {
synchronized (this) {
- if (Thread.currentThread().isInterrupted()) {
+ if (skipOnInterrupt && Thread.currentThread().isInterrupted()) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@@ -290,6 +287,14 @@ public class XmppConnection implements Runnable {
this.accountStateProcessor.accept(nextStatus);
}
+ private void changeStateTerminal(final Account.State state) {
+ // interrupt needs to be called before status change; otherwise we interrupt the newly
+ // created thread
+ this.interrupt();
+ this.forceCloseSocket();
+ this.changeState(state, false);
+ }
+
public Jid getJidForCommand(final String node) {
synchronized (this.commands) {
return this.commands.get(node);
@@ -301,7 +306,7 @@ public class XmppConnection implements Runnable {
this.lastPingSent = SystemClock.elapsedRealtime();
this.lastDiscoStarted = Long.MAX_VALUE;
this.mWaitingForSmCatchup.set(false);
- this.changeStatus(Account.State.CONNECTING);
+ this.changeState(Account.State.CONNECTING);
}
public boolean isWaitingForSmCatchup() {
@@ -331,7 +336,7 @@ public class XmppConnection implements Runnable {
try {
Socket localSocket;
shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER);
- this.changeStatus(Account.State.CONNECTING);
+ this.changeState(Account.State.CONNECTING);
final boolean useTorSetting = appSettings.isUseTor();
final boolean extended = appSettings.isExtendedConnectionOptions();
final boolean useTor = useTorSetting || account.isOnion();
@@ -533,27 +538,27 @@ public class XmppConnection implements Runnable {
}
processStream();
} catch (final SecurityException e) {
- this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
+ this.changeState(Account.State.MISSING_INTERNET_PERMISSION);
} catch (final StateChangingException e) {
- this.changeStatus(e.state);
+ this.changeState(e.state);
} catch (final UnknownHostException
| ConnectException
| SocksSocketFactory.HostNotFoundException e) {
- this.changeStatus(Account.State.SERVER_NOT_FOUND);
+ this.changeState(Account.State.SERVER_NOT_FOUND);
} catch (final SocksSocketFactory.SocksProxyNotFoundException e) {
- this.changeStatus(Account.State.TOR_NOT_AVAILABLE);
+ this.changeState(Account.State.TOR_NOT_AVAILABLE);
} catch (final IOException | XmlPullParserException e) {
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": " + e.getMessage());
- this.changeStatus(Account.State.OFFLINE);
+ this.changeState(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1);
} finally {
- if (!Thread.currentThread().isInterrupted()) {
- forceCloseSocket();
- } else {
+ if (Thread.currentThread().isInterrupted()) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": not force closing socket because thread was interrupted");
+ } else {
+ forceCloseSocket();
}
}
}
@@ -1189,7 +1194,7 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": awaiting disco results after resume");
- changeStatus(Account.State.CONNECTING);
+ changeState(Account.State.CONNECTING);
} else {
changeStatusToOnline();
}
@@ -1199,7 +1204,7 @@ public class XmppConnection implements Runnable {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid() + ": online with resource " + account.getResource());
- changeStatus(Account.State.ONLINE);
+ changeState(Account.State.ONLINE);
}
private void processFailed(final Failed failed, final boolean sendBindRequest) {
@@ -1539,12 +1544,12 @@ public class XmppConnection implements Runnable {
mXmppConnectionService.databaseBackend.updateAccount(account);
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
- if (streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
- && account.isOptionSet(Account.OPTION_REGISTER)) {
- register();
- } else if (!streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
- && account.isOptionSet(Account.OPTION_REGISTER)) {
- throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
+ if (account.isOptionSet(Account.OPTION_REGISTER)) {
+ if (this.streamFeatures.register()) {
+ this.register();
+ } else {
+ throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED);
+ }
} else if (streamFeatures.hasStreamFeature(Authentication.class)
&& shouldAuthenticate
&& this.loginInfo == null) {
@@ -1847,167 +1852,95 @@ public class XmppConnection implements Runnable {
}
private void register() {
- final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN);
- if (preAuth != null && features.invite()) {
- final Iq preAuthRequest = new Iq(Iq.Type.SET);
- preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
- sendUnmodifiedIqPacket(
- preAuthRequest,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- sendRegistryRequest();
+ final String preAuthToken =
+ Strings.emptyToNull(account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN));
+ final ListenableFuture<RegistrationManager.Registration> registrationFuture;
+ if (preAuthToken != null && streamFeatures.preAuthenticatedInBandRegistration()) {
+ registrationFuture =
+ getManager(RegistrationManager.class).getRegistration(preAuthToken);
+ } else {
+ registrationFuture = getManager(RegistrationManager.class).getRegistration();
+ }
+ // TODO should we store this future and cancel it during disconnect or something
+ Futures.addCallback(
+ registrationFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final RegistrationManager.Registration registration) {
+ if (registration instanceof RegistrationManager.SimpleRegistration) {
+ final var future = getManager(RegistrationManager.class).register();
+ awaitRegistrationResponse(future);
+ } else if (registration
+ instanceof RegistrationManager.ExtendedRegistration er) {
+ mXmppConnectionService.displayCaptchaRequest(
+ account, er.getData(), er.getCaptcha());
+ } else if (registration
+ instanceof
+ RegistrationManager.RedirectRegistration redirectRegistration) {
+ XmppConnection.this.redirectionUrl = redirectRegistration.getURL();
+ changeStateTerminal(Account.State.REGISTRATION_WEB);
} else {
- final String error = response.getErrorCondition();
Log.d(
Config.LOGTAG,
- account.getJid().asBareJid()
- + ": failed to pre auth. "
- + error);
- throw new StateChangingError(Account.State.REGISTRATION_INVALID_TOKEN);
+ "got registration: " + registration.getClass().getName());
+ changeStateTerminal(Account.State.REGISTRATION_NOT_SUPPORTED);
}
- },
- true);
- } else {
- sendRegistryRequest();
- }
- }
-
- private void sendRegistryRequest() {
- final Iq register = new Iq(Iq.Type.GET);
- register.query(Namespace.REGISTER);
- register.setTo(account.getDomain());
- sendUnmodifiedIqPacket(
- register,
- (packet) -> {
- if (packet.getType() == Iq.Type.TIMEOUT) {
- return;
- }
- if (packet.getType() == Iq.Type.ERROR) {
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
}
- final Element query = packet.query(Namespace.REGISTER);
- if (query.hasChild("username") && (query.hasChild("password"))) {
- final Iq register1 = new Iq(Iq.Type.SET);
- final Element username =
- new Element("username").setContent(account.getUsername());
- final Element password =
- new Element("password").setContent(account.getPassword());
- register1.query(Namespace.REGISTER).addChild(username);
- register1.query().addChild(password);
- register1.setFrom(account.getJid().asBareJid());
- sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true);
- } else if (query.hasChild("x", Namespace.DATA)) {
- final Data data = Data.parse(query.findChild("x", Namespace.DATA));
- final Element blob = query.findChild("data", "urn:xmpp:bob");
- final String id = packet.getId();
- InputStream is;
- if (blob != null) {
- try {
- final String base64Blob = blob.getContent();
- final byte[] strBlob = Base64.decode(base64Blob, Base64.DEFAULT);
- is = new ByteArrayInputStream(strBlob);
- } catch (Exception e) {
- is = null;
- }
- } else {
- final boolean useTor = this.appSettings.isUseTor() || account.isOnion();
- try {
- final String url = data.getValue("url");
- final String fallbackUrl = data.getValue("captcha-fallback-url");
- if (url != null) {
- is = HttpConnectionManager.open(url, useTor);
- } else if (fallbackUrl != null) {
- is = HttpConnectionManager.open(fallbackUrl, useTor);
- } else {
- is = null;
- }
- } catch (final IOException e) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": unable to fetch captcha",
- e);
- is = null;
- }
- }
- if (is != null) {
- Bitmap captcha = BitmapFactory.decodeStream(is);
- try {
- if (mXmppConnectionService.displayCaptchaRequest(
- account, id, data, captcha)) {
- return;
- }
- } catch (Exception e) {
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
- }
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof TimeoutException) {
+ return;
}
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
- } else if (query.hasChild("instructions")
- || query.hasChild("x", Namespace.OOB)) {
- final String instructions = query.findChildContent("instructions");
- final Element oob = query.findChild("x", Namespace.OOB);
- final String url = oob == null ? null : oob.findChildContent("url");
- if (url != null) {
- setAccountCreationFailed(url);
- } else if (instructions != null) {
- final Matcher matcher = Patterns.URI_HTTP.matcher(instructions);
- if (matcher.find()) {
- setAccountCreationFailed(
- instructions.substring(matcher.start(), matcher.end()));
- }
+ if (t instanceof RegistrationManager.InvalidTokenException) {
+ changeStateTerminal(Account.State.REGISTRATION_INVALID_TOKEN);
+ } else {
+ changeStateTerminal(Account.State.REGISTRATION_FAILED);
}
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
}
},
- true);
+ MoreExecutors.directExecutor());
}
- public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) {
- final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data);
- this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true);
+ public void register(
+ final im.conversations.android.xmpp.model.data.Data data, final String ocr) {
+ final var future = getManager(RegistrationManager.class).register(data, ocr);
+ awaitRegistrationResponse(future);
}
- private void processRegistrationResponse(final Iq response) {
- if (response.getType() == Iq.Type.RESULT) {
- account.setOption(Account.OPTION_REGISTER, false);
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": successfully registered new account on server");
- throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
- } else {
- final Account.State state = getRegistrationFailedState(response);
- throw new StateChangingError(state);
- }
- }
-
- @NonNull
- private static Account.State getRegistrationFailedState(final Iq response) {
- final List<String> PASSWORD_TOO_WEAK_MESSAGES =
- Arrays.asList("The password is too weak", "Please use a longer password.");
- final var error = response.getError();
- final var condition = error == null ? null : error.getCondition();
- final Account.State state;
- if (condition instanceof Condition.Conflict) {
- state = Account.State.REGISTRATION_CONFLICT;
- } else if (condition instanceof Condition.ResourceConstraint) {
- state = Account.State.REGISTRATION_PLEASE_WAIT;
- } else if (condition instanceof Condition.NotAcceptable
- && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
- state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
- } else {
- state = Account.State.REGISTRATION_FAILED;
- }
- return state;
+ private void awaitRegistrationResponse(final ListenableFuture<Void> registration) {
+ Futures.addCallback(
+ registration,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ account.setOption(Account.OPTION_REGISTER, false);
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": successfully registered new account on server");
+ changeStateTerminal(Account.State.REGISTRATION_SUCCESSFUL);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ if (t instanceof TimeoutException) {
+ return;
+ }
+ if (t
+ instanceof
+ RegistrationManager.RegistrationFailedException exception) {
+ changeStateTerminal(exception.asAccountState());
+ } else {
+ changeStateTerminal(Account.State.REGISTRATION_FAILED);
+ }
+ }
+ },
+ MoreExecutors.directExecutor());
}
- private void setAccountCreationFailed(final String url) {
- final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url);
- if (httpUrl != null && httpUrl.isHttps()) {
- this.redirectionUrl = httpUrl;
- throw new StateChangingError(Account.State.REGISTRATION_WEB);
- }
- throw new StateChangingError(Account.State.REGISTRATION_FAILED);
+ public void cancelRegistration() {
+ this.changeStateTerminal(Account.State.REGISTRATION_FAILED);
}
public HttpUrl getRedirectionUrl() {
@@ -2608,6 +2541,7 @@ public class XmppConnection implements Runnable {
return;
}
synchronized (this.mStanzaQueue) {
+ // TODO should we fail IQs for unbound streams?
if (force || isBound) {
tagWriter.writeStanzaAsync(packet);
} else {
@@ -2843,6 +2777,9 @@ public class XmppConnection implements Runnable {
}
public void triggerConnectionTimeout() {
+
+ // TODO not triggering timeout while waiting for captcha input
+
final var duration = getConnectionDuration();
Log.d(
Config.LOGTAG,
@@ -2851,11 +2788,7 @@ public class XmppConnection implements Runnable {
// last connection time gets reset so time to next attempt is calculated correctly
this.lastConnectionStarted = SystemClock.elapsedRealtime();
- // interrupt needs to be called before status change; otherwise we interrupt the newly
- // created thread
- this.interrupt();
- this.forceCloseSocket();
- this.changeStatus(Account.State.CONNECTION_TIMEOUT);
+ this.changeStateTerminal(Account.State.CONNECTION_TIMEOUT);
}
public Account getAccount() {
@@ -3088,15 +3021,6 @@ public class XmppConnection implements Runnable {
account.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL);
}
- public boolean register() {
- return hasDiscoFeature(account.getDomain(), Namespace.REGISTER);
- }
-
- public boolean invite() {
- return connection.streamFeatures != null
- && connection.streamFeatures.hasChild("register", Namespace.INVITE);
- }
-
public boolean sm() {
return streamId != null
|| (connection.streamFeatures != null
@@ -3108,6 +3032,7 @@ public class XmppConnection implements Runnable {
&& connection.streamFeatures.clientStateIndication();
}
+ // TODO move to manager
public boolean pep() {
final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep");
@@ -1,14 +1,24 @@
package eu.siacs.conversations.xmpp.manager;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
import android.util.Patterns;
import androidx.annotation.NonNull;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.IqErrorException;
import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.error.Condition;
import im.conversations.android.xmpp.model.oob.OutOfBandData;
import im.conversations.android.xmpp.model.pars.PreAuth;
import im.conversations.android.xmpp.model.register.Instructions;
@@ -18,6 +28,7 @@ import im.conversations.android.xmpp.model.register.Remove;
import im.conversations.android.xmpp.model.register.Username;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.util.Arrays;
+import java.util.List;
import java.util.regex.Matcher;
import okhttp3.HttpUrl;
@@ -35,7 +46,63 @@ public class RegistrationManager extends AbstractManager {
register.addUsername(account.getJid().getLocal());
register.addPassword(password);
return Futures.transform(
- connection.sendIqPacket(iq), r -> null, MoreExecutors.directExecutor());
+ connection.sendIqPacket(iq),
+ r -> {
+ account.setPassword(password);
+ account.setOption(Account.OPTION_MAGIC_CREATE, false);
+ getDatabase().updateAccount(account);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> register() {
+ final var account = getAccount();
+ final var iq = new Iq(Iq.Type.SET);
+ iq.setTo(account.getJid().getDomain());
+ final var register = iq.addExtension(new Register());
+ register.addUsername(account.getJid().getLocal());
+ register.addPassword(account.getPassword());
+ final ListenableFuture<Void> future =
+ Futures.transform(
+ connection.sendIqPacket(iq, true),
+ result -> null,
+ MoreExecutors.directExecutor());
+ return Futures.catchingAsync(
+ future,
+ IqErrorException.class,
+ ex ->
+ Futures.immediateFailedFuture(
+ new RegistrationFailedException(ex.getResponse())),
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Void> register(final Data data, final String ocr) {
+ final var account = getAccount();
+ final var submission =
+ data.submit(
+ ImmutableMap.of(
+ "username",
+ account.getJid().getLocal(),
+ "password",
+ account.getPassword(),
+ "ocr",
+ ocr));
+ final var iq = new Iq(Iq.Type.SET);
+ final var register = iq.addExtension(new Register());
+ register.addExtension(submission);
+ final ListenableFuture<Void> future =
+ Futures.transform(
+ connection.sendIqPacket(iq, true),
+ result -> null,
+ MoreExecutors.directExecutor());
+ return Futures.catchingAsync(
+ future,
+ IqErrorException.class,
+ ex ->
+ Futures.immediateFailedFuture(
+ new RegistrationFailedException(ex.getResponse())),
+ MoreExecutors.directExecutor());
}
public ListenableFuture<Void> unregister() {
@@ -54,7 +121,7 @@ public class RegistrationManager extends AbstractManager {
iq.setTo(account.getDomain());
iq.addExtension(new Register());
final var future = connection.sendIqPacket(iq, true);
- return Futures.transform(
+ return Futures.transformAsync(
future,
result -> {
final var register = result.getExtension(Register.class);
@@ -64,15 +131,18 @@ public class RegistrationManager extends AbstractManager {
}
if (register.hasExtension(Username.class)
&& register.hasExtension(Password.class)) {
- return new SimpleRegistration();
+ return Futures.immediateFuture(new SimpleRegistration());
}
+
+ // find bits of binary and get captcha from there
+
final var data = register.getExtension(Data.class);
// note that the captcha namespace is incorrect here. That namespace is only
// used in message challenges. ejabberd uses the incorrect namespace though
if (data != null
&& Arrays.asList(Namespace.REGISTER, Namespace.CAPTCHA)
.contains(data.getFormType())) {
- return new ExtendedRegistration(data);
+ return getExtendedRegistration(register, data);
}
final var oob = register.getExtension(OutOfBandData.class);
final var instructions = register.getExtension(Instructions.class);
@@ -80,14 +150,15 @@ public class RegistrationManager extends AbstractManager {
instructions == null ? null : instructions.getContent();
final String redirectUrl = oob == null ? null : oob.getURL();
if (redirectUrl != null) {
- return RedirectRegistration.ifValid(redirectUrl);
+ return Futures.immediateFuture(RedirectRegistration.ifValid(redirectUrl));
}
if (instructionsText != null) {
final Matcher matcher = Patterns.WEB_URL.matcher(instructionsText);
if (matcher.find()) {
final String instructionsUrl =
instructionsText.substring(matcher.start(), matcher.end());
- return RedirectRegistration.ifValid(instructionsUrl);
+ return Futures.immediateFuture(
+ RedirectRegistration.ifValid(instructionsUrl));
}
}
throw new IllegalStateException("No supported registration method found");
@@ -95,6 +166,59 @@ public class RegistrationManager extends AbstractManager {
MoreExecutors.directExecutor());
}
+ private ListenableFuture<Registration> getExtendedRegistration(
+ final Register register, final Data data) {
+ final var ocr = data.getFieldByName("ocr");
+ if (ocr == null) {
+ throw new IllegalArgumentException("Missing OCR form field");
+ }
+ final var ocrMedia = ocr.getMedia();
+ if (ocrMedia == null) {
+ throw new IllegalArgumentException("OCR form field missing media");
+ }
+ final var uris = ocrMedia.getUris();
+ final var bobUri = Iterables.find(uris, u -> "cid".equals(u.getScheme()), null);
+ final Optional<im.conversations.android.xmpp.model.bob.Data> bob;
+ if (bobUri != null) {
+ bob = im.conversations.android.xmpp.model.bob.Data.get(register, bobUri.getPath());
+ } else {
+ bob = Optional.absent();
+ }
+ if (bob.isPresent()) {
+ final var bytes = bob.get().asBytes();
+ final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ return Futures.immediateFuture(new ExtendedRegistration(bitmap, data));
+ }
+ final var captchaFallbackUrl = data.getValue("captcha-fallback-url");
+ if (captchaFallbackUrl == null) {
+ throw new IllegalStateException("No captcha fallback URL provided");
+ }
+ final var captchFallbackHttpUrl = HttpUrl.parse(captchaFallbackUrl);
+ Log.d(Config.LOGTAG, "fallback url: " + captchFallbackHttpUrl);
+ throw new IllegalStateException("Not implemented");
+ }
+
+ public ListenableFuture<Registration> getRegistration(final String token) {
+ final var preAuthentication = sendPreAuthentication(token);
+ final var caught =
+ Futures.catchingAsync(
+ preAuthentication,
+ IqErrorException.class,
+ ex -> {
+ final var error = ex.getError();
+ final var condition = error == null ? null : error.getCondition();
+ if (condition instanceof Condition.ItemNotFound) {
+ return Futures.immediateFailedFuture(
+ new InvalidTokenException(ex.getResponse()));
+ } else {
+ return Futures.immediateFuture(ex);
+ }
+ },
+ MoreExecutors.directExecutor());
+ return Futures.transformAsync(
+ caught, v -> getRegistration(), MoreExecutors.directExecutor());
+ }
+
public ListenableFuture<Void> sendPreAuthentication(final String token) {
final var account = getAccount();
final var iq = new Iq(Iq.Type.GET);
@@ -105,6 +229,10 @@ public class RegistrationManager extends AbstractManager {
return Futures.transform(future, result -> null, MoreExecutors.directExecutor());
}
+ public boolean hasFeature() {
+ return getManager(DiscoManager.class).hasServerFeature(Namespace.REGISTER);
+ }
+
public abstract static class Registration {}
// only requires Username + Password
@@ -112,12 +240,18 @@ public class RegistrationManager extends AbstractManager {
// Captcha as shown here: https://xmpp.org/extensions/xep-0158.html#register
public static class ExtendedRegistration extends Registration {
+ private final Bitmap captcha;
private final Data data;
- public ExtendedRegistration(Data data) {
+ public ExtendedRegistration(final Bitmap captcha, final Data data) {
+ this.captcha = captcha;
this.data = data;
}
+ public Bitmap getCaptcha() {
+ return this.captcha;
+ }
+
public Data getData() {
return this.data;
}
@@ -144,4 +278,36 @@ public class RegistrationManager extends AbstractManager {
"A URL found the registration instructions is not valid");
}
}
+
+ public static class InvalidTokenException extends IqErrorException {
+
+ public InvalidTokenException(final Iq response) {
+ super(response);
+ }
+ }
+
+ public static class RegistrationFailedException extends IqErrorException {
+
+ private final List<String> PASSWORD_TOO_WEAK_MESSAGES =
+ Arrays.asList("The password is too weak", "Please use a longer password.");
+
+ public RegistrationFailedException(final Iq response) {
+ super(response);
+ }
+
+ public Account.State asAccountState() {
+ final var error = getError();
+ final var condition = error == null ? null : error.getCondition();
+ if (condition instanceof Condition.Conflict) {
+ return Account.State.REGISTRATION_CONFLICT;
+ } else if (condition instanceof Condition.ResourceConstraint) {
+ return Account.State.REGISTRATION_PLEASE_WAIT;
+ } else if (condition instanceof Condition.NotAcceptable
+ && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) {
+ return Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
+ } else {
+ return Account.State.REGISTRATION_FAILED;
+ }
+ }
+ }
}
@@ -0,0 +1,27 @@
+package im.conversations.android.xmpp.model.bob;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Iterables;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Data extends Extension implements ByteContent {
+
+ public Data() {
+ super(Data.class);
+ }
+
+ public String getCid() {
+ return this.getAttribute("cid");
+ }
+
+ public String getType() {
+ return this.getAttribute("type");
+ }
+
+ public static Optional<Data> get(final Extension stanza, final String cid) {
+ return Iterables.tryFind(stanza.getExtensions(Data.class), d -> cid.equals(d.getCid()));
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.BOB)
+package im.conversations.android.xmpp.model.bob;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -5,6 +5,7 @@ import com.google.common.collect.Iterables;
import eu.siacs.conversations.xml.Element;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.media.Media;
import java.util.Collection;
@XmlElement
@@ -32,4 +33,8 @@ public class Field extends Extension {
public void setType(String type) {
this.setAttribute("type", type);
}
+
+ public Media getMedia() {
+ return getOnlyExtension(Media.class);
+ }
}
@@ -0,0 +1,25 @@
+package im.conversations.android.xmpp.model.media;
+
+import com.google.common.collect.Collections2;
+import de.gultsch.common.MiniUri;
+import eu.siacs.conversations.xml.Element;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
+import java.util.Objects;
+
+@XmlElement
+public class Media extends Extension {
+
+ public Media() {
+ super(Media.class);
+ }
+
+ public Collection<MiniUri> getUris() {
+ final var uris =
+ Collections2.filter(
+ Collections2.transform(this.getExtensions(Uri.class), Element::getContent),
+ Objects::nonNull);
+ return Collections2.transform(uris, MiniUri::new);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.media;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Uri extends Extension {
+
+ public Uri() {
+ super(Uri.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.MEDIA_ELEMENT)
+package im.conversations.android.xmpp.model.media;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.register;
+import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+@XmlElement
public class Instructions extends Extension {
public Instructions() {
@@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.register;
+import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+@XmlElement
public class Password extends Extension {
public Password() {
@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.register;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+@XmlElement(name = "register", namespace = Namespace.REGISTER_STREAM_FEATURE)
+public class RegisterStreamFeature extends StreamFeature {
+
+ public RegisterStreamFeature() {
+ super(RegisterStreamFeature.class);
+ }
+}
@@ -1,7 +1,9 @@
package im.conversations.android.xmpp.model.register;
+import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+@XmlElement
public class Remove extends Extension {
public Remove() {
@@ -1,12 +1,13 @@
package im.conversations.android.xmpp.model.streams;
+import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
-import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.StreamElement;
import im.conversations.android.xmpp.model.StreamFeature;
import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
-import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.register.RegisterStreamFeature;
import im.conversations.android.xmpp.model.sm.StreamManagement;
+import im.conversations.android.xmpp.model.token.Register;
@XmlElement
public class Features extends StreamElement implements EntityCapabilities {
@@ -18,14 +19,17 @@ public class Features extends StreamElement implements EntityCapabilities {
return hasStreamFeature(StreamManagement.class);
}
- public boolean invite() {
- return this.hasChild("register", Namespace.INVITE);
- }
-
public boolean clientStateIndication() {
return this.hasChild("csi", Namespace.CSI);
}
+ public boolean register() {
+ return hasStreamFeature(RegisterStreamFeature.class);
+ }
+
+ public boolean preAuthenticatedInBandRegistration() {
+ return hasStreamFeature(Register.class);
+ }
public boolean hasStreamFeature(final Class<? extends StreamFeature> clazz) {
return hasExtension(clazz);
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.token;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.StreamFeature;
+
+@XmlElement
+public class Register extends StreamFeature {
+
+ public Register() {
+ super(Register.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.PRE_AUTHENTICATED_IN_BAND_REGISTRATION)
+package im.conversations.android.xmpp.model.token;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;