Account Setup flow rework

This commit is contained in:
Paul Schaub 2020-11-09 15:23:22 +01:00
parent 7d6b75b326
commit 067ea4b65b
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
40 changed files with 941 additions and 81 deletions

View File

@ -104,6 +104,10 @@ dependencies {
implementation "com.jakewharton:butterknife:$butterKnifeVersion"
annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnifeVersion"
// RxBinding
implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0'
implementation 'com.jakewharton.rxbinding4:rxbinding-core:4.0.0'
// support libraries
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation 'com.google.android.material:material:1.2.0-alpha03'

View File

@ -17,7 +17,9 @@
android:roundIcon="@drawable/ic_mercury_icon"
android:supportsRtl="true"
android:theme="@style/Theme.Mercury">
<activity android:name=".android.ui.ikey.IkeyBackupRestoreActivity"></activity>
<activity android:name=".android.crypto.ikey.IkeyBackupRestoreActivity">
</activity>
<activity
android:name=".android.ui.chat.ChatActivity"
android:label="Chat" />
@ -35,6 +37,7 @@
android:label="@string/title_activity_settings" />
<activity android:name=".android.ui.contacts.detail.ContactDetailActivity" />
<activity android:name=".android.ui.account.detail.AccountDetailsActivity" />
<activity android:name=".android.ui.account.login.AddAccountActivity" />
<service android:name=".android.service.MercuryForegroundService" />
</application>

View File

@ -5,7 +5,6 @@ import android.content.Intent;
import android.os.Build;
import org.jivesoftware.smack.android.AndroidSmackInitializer;
import org.jivesoftware.smackx.ping.android.ServerPingWithAlarmManager;
import org.mercury_im.messenger.android.di.component.DaggerAppComponent;
import org.mercury_im.messenger.android.util.AndroidLoggingHandler;
import org.mercury_im.messenger.core.Messenger;

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.android.ui.ikey;
package org.mercury_im.messenger.android.crypto.ikey;
import android.graphics.Bitmap;
@ -11,6 +11,7 @@ import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.android.util.QrCodeGenerator;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.crypto.OpenPgpSecretKeyBackupPassphraseGenerator;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.ikey.IkeySecretKeyBackupCreationViewModel;
@ -22,9 +23,9 @@ import javax.inject.Inject;
import io.reactivex.Observable;
public class IkeyBackupCreationAndroidViewModel extends ViewModel implements MercuryAndroidViewModel<IkeySecretKeyBackupCreationViewModel> {
public class AndroidIkeyBackupCreationViewModel extends ViewModel implements MercuryAndroidViewModel<IkeySecretKeyBackupCreationViewModel> {
private static final Logger LOGGER = Logger.getLogger(IkeyBackupCreationAndroidViewModel.class.getName());
private static final Logger LOGGER = Logger.getLogger(AndroidIkeyBackupCreationViewModel.class.getName());
MutableLiveData<OpenPgpSecretKeyBackupPassphrase> passphrase = new MutableLiveData<>();
MutableLiveData<Bitmap> passphraseAsQrCode = new MutableLiveData<>();
@ -32,10 +33,13 @@ public class IkeyBackupCreationAndroidViewModel extends ViewModel implements Mer
@Inject
IkeySecretKeyBackupCreationViewModel commonViewModel;
@Inject
OpenPgpSecretKeyBackupPassphraseGenerator passphraseGenerator;
@Inject
SchedulersFacade schedulers;
public IkeyBackupCreationAndroidViewModel() {
public AndroidIkeyBackupCreationViewModel() {
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
@ -43,8 +47,8 @@ public class IkeyBackupCreationAndroidViewModel extends ViewModel implements Mer
getCommonViewModel().setAccountId(accountId);
Observable<Optional<OpenPgpSecretKeyBackupPassphrase>> passphraseObservable =
//getCommonViewModel().getPassphrase()
Observable.just(new Optional<>(new OpenPgpSecretKeyBackupPassphrase("71ZA-Y416-UA7A-7NCE-3SNM-88EF")));
getCommonViewModel().getPassphrase();
//Observable.just(new Optional<>(passphraseGenerator.generateBackupPassphrase()));
addDisposable(passphraseObservable
.subscribeOn(schedulers.getIoScheduler())

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.android.ui.ikey;
package org.mercury_im.messenger.android.crypto.ikey;
import androidx.lifecycle.ViewModelProvider;
@ -23,7 +23,7 @@ import butterknife.ButterKnife;
public class IkeyBackupCreationFragment extends Fragment {
private IkeyBackupCreationAndroidViewModel viewModel;
private AndroidIkeyBackupCreationViewModel viewModel;
@BindView(R.id.backup_code)
TextView backupCode;
@ -52,7 +52,7 @@ public class IkeyBackupCreationFragment extends Fragment {
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
viewModel = new ViewModelProvider(this).get(IkeyBackupCreationAndroidViewModel.class);
viewModel = new ViewModelProvider(this).get(AndroidIkeyBackupCreationViewModel.class);
viewModel.initialize(accountId);
viewModel.getPassphrase().observe(getViewLifecycleOwner(), passphrase -> backupCode.setText(passphrase));

View File

@ -1,4 +1,4 @@
package org.mercury_im.messenger.android.ui.ikey;
package org.mercury_im.messenger.android.crypto.ikey;
import androidx.appcompat.app.AppCompatActivity;
@ -11,6 +11,6 @@ public class IkeyBackupRestoreActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ikey_backup_restore);
setContentView(R.layout.fragment_ikey_backup_restore);
}
}

View File

@ -4,8 +4,12 @@ import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.di.module.AndroidDatabaseModule;
import org.mercury_im.messenger.android.di.module.AndroidSchedulersModule;
import org.mercury_im.messenger.android.ui.account.detail.AndroidAccountDetailsViewModel;
import org.mercury_im.messenger.android.ui.account.login.AddAccountActivity;
import org.mercury_im.messenger.android.ui.account.login.EnterAccountDetailsFragment;
import org.mercury_im.messenger.android.ui.account.login.IkeySetupViewModel;
import org.mercury_im.messenger.android.ui.contacts.AndroidContactListViewModel;
import org.mercury_im.messenger.android.ui.ikey.IkeyBackupCreationAndroidViewModel;
import org.mercury_im.messenger.android.crypto.ikey.AndroidIkeyBackupCreationViewModel;
import org.mercury_im.messenger.core.di.module.IkeyModule;
import org.mercury_im.messenger.core.di.module.OpenPgpModule;
import org.mercury_im.messenger.core.di.module.RxMercuryMessageStoreFactoryModule;
import org.mercury_im.messenger.core.di.module.RxMercuryRosterStoreFactoryModule;
@ -50,6 +54,7 @@ import dagger.Component;
XmppTcpConnectionFactoryModule.class,
RxMercuryMessageStoreFactoryModule.class,
OpenPgpModule.class,
IkeyModule.class,
RxMercuryRosterStoreFactoryModule.class,
StanzaIdSourceFactoryModule.class
})
@ -70,7 +75,7 @@ public interface AppComponent {
void inject(ContactDetailActivity contactDetailActivity);
void inject(EnterAccountDetailsFragment enterAccountDetailsFragment);
// ViewModels
@ -94,10 +99,13 @@ public interface AppComponent {
void inject(AndroidAccountDetailsViewModel accountDetailsViewModel);
void inject(IkeyBackupCreationAndroidViewModel ikeyBackupCreationAndroidViewModel);
void inject(AndroidIkeyBackupCreationViewModel androidIkeyBackupCreationViewModel);
void inject(IkeySetupViewModel ikeySetupViewModel);
// void inject(AndroidOxSecretKeyBackupRestoreViewModel androidOxSecretKeyBackupRestoreViewModel);
// Common VMs
// Common VMs
void inject(LoginViewModel loginViewModel);
void inject(AccountListViewModel accountsViewModel);
@ -108,4 +116,5 @@ public interface AppComponent {
void inject(MercuryForegroundService service);
void inject(MercuryEntityCapsStore store);
}

View File

@ -11,11 +11,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
@ -24,7 +23,7 @@ import com.google.android.material.card.MaterialCardView;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.ikey.IkeyBackupCreationFragment;
import org.mercury_im.messenger.android.crypto.ikey.IkeyBackupCreationFragment;
import org.mercury_im.messenger.android.ui.openpgp.ToggleableFingerprintsAdapter;
import org.mercury_im.messenger.android.ui.openpgp.OpenPgpV4FingerprintFormatter;
import org.mercury_im.messenger.core.util.Optional;
@ -46,6 +45,9 @@ public class AccountDetailsFragment extends Fragment {
@BindView(R.id.jid)
TextView jid;
@BindView(R.id.btn_backup)
Button localKeyCreateBackupButton;
@BindView(R.id.btn_share)
Button localFingerprintShareButton;
@ -154,6 +156,15 @@ public class AccountDetailsFragment extends Fragment {
getParentFragmentManager().beginTransaction()
.replace(R.id.fragment, IkeyBackupCreationFragment.newInstance(accountId)).commit();
});
localKeyCreateBackupButton.setOnClickListener(v -> {
Toast.makeText(getContext(), "Not yet implemented.", Toast.LENGTH_SHORT).show();
});
viewModel.isAccountAuthenticated().observe(this, authenticated -> {
ikeyCreateBackupButton.setEnabled(authenticated);
localKeyCreateBackupButton.setEnabled(authenticated);
});
}
private void startShareFingerprintIntent(OpenPgpV4Fingerprint fingerprint) {

View File

@ -18,6 +18,7 @@ import org.mercury_im.messenger.core.util.DefaultUtil;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.account.detail.AccountDetailsViewModel;
import org.mercury_im.messenger.core.viewmodel.openpgp.FingerprintViewItem;
import org.mercury_im.messenger.entity.Account;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.ArrayList;
@ -46,6 +47,8 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
private MutableLiveData<OpenPgpV4Fingerprint> localFingerprint = new MutableLiveData<>();
private MutableLiveData<List<FingerprintViewItem>> remoteFingerprints = new MutableLiveData<>(new ArrayList<>());
private MutableLiveData<EntityBareJid> jid = new MutableLiveData<>(DefaultUtil.defaultJid());
private MutableLiveData<Boolean> accountEnabled = new MutableLiveData<>(false);
private MutableLiveData<Boolean> accountAuthenticated = new MutableLiveData<>(false);
public AndroidAccountDetailsViewModel(@NonNull Application application, UUID accountId) {
super(application);
@ -75,6 +78,18 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
() -> LOGGER.log(Level.INFO, "observing remote fingerprint onComplete.")));
addDisposable(getCommonViewModel().getJid(accountId).subscribe(jid::postValue));
addDisposable(getCommonViewModel().isAccountEnabled(accountId)
.compose(schedulers.executeUiSafeObservable())
.subscribe(bool -> accountEnabled.postValue(bool),
e -> LOGGER.log(Level.SEVERE, "Error subscribing to account enabled state", e),
() -> LOGGER.log(Level.INFO, "observing account enabled state onComplete.")));
addDisposable(getCommonViewModel().isAccountAuthenticated(accountId)
.compose(schedulers.executeUiSafeObservable())
.subscribe(bool -> accountAuthenticated.postValue(bool),
e -> LOGGER.log(Level.SEVERE, "Error subscribing to account authenticated state", e),
() -> LOGGER.log(Level.INFO, "observing account authenticated state onComplete.")));
}
@Override
@ -112,10 +127,10 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
public void onGenerateIkey() {
addDisposable(getCommonViewModel().generateIkey(accountId)
.subscribeOn(schedulers.getNewThread())
.observeOn(schedulers.getUiScheduler())
.subscribe(() -> LOGGER.log(Level.INFO, "IKey generated for account " + accountId),
e -> LOGGER.log(Level.SEVERE, "Could not generate Ikey", e)));
.subscribeOn(schedulers.getNewThread())
.observeOn(schedulers.getUiScheduler())
.subscribe(() -> LOGGER.log(Level.INFO, "IKey generated for account " + accountId),
e -> LOGGER.log(Level.SEVERE, "Could not generate Ikey", e)));
}
public void onDeleteIkey() {
@ -134,6 +149,14 @@ public class AndroidAccountDetailsViewModel extends AndroidViewModel implements
e -> LOGGER.log(Level.SEVERE, "Could not restore Ikey backup", e)));
}
public LiveData<Boolean> isAccountEnabled() {
return accountEnabled;
}
public LiveData<Boolean> isAccountAuthenticated() {
return accountAuthenticated;
}
public static class AndroidAccountDetailsViewModelFactory implements ViewModelProvider.Factory {
private final Application application;

View File

@ -1,6 +1,7 @@
package org.mercury_im.messenger.android.ui.account.list;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -16,6 +17,7 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.ui.account.OnAccountListItemClickListener;
import org.mercury_im.messenger.android.ui.account.login.AddAccountActivity;
import org.mercury_im.messenger.android.ui.account.login.AddAccountDialogFragment;
import java.util.ArrayList;
@ -58,7 +60,10 @@ public class AccountListFragment extends Fragment implements SearchView.OnQueryT
public void onResume() {
super.onResume();
observeViewModel();
fab.setOnClickListener(v -> displayAddAccountDialog());
//fab.setOnClickListener(v -> displayAddAccountDialog());
fab.setOnClickListener(v -> {
startActivity(new Intent(getContext(), AddAccountActivity.class));
});
}
private void observeViewModel() {

View File

@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.jetbrains.annotations.NotNull;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.ui.account.OnAccountListItemClickListener;
import org.mercury_im.messenger.core.connection.state.ConnectivityState;
import org.mercury_im.messenger.core.viewmodel.account.list.AccountViewItem;
import org.mercury_im.messenger.entity.Account;
import org.mercury_im.messenger.android.ui.avatar.AvatarDrawable;
@ -62,8 +63,9 @@ public class AccountListRecyclerViewAdapter extends RecyclerView.Adapter<Account
holder.jid.setText(account.getAddress());
holder.avatar.setImageDrawable(new AvatarDrawable(account.getAddress(), account.getAddress()));
holder.enabled.setChecked(account.isEnabled());
holder.enabled.setOnCheckedChangeListener((compoundButton, checked) -> {
if (!compoundButton.isPressed()) {
holder.enabled.setEnabled(shouldSwitchBeEnabled(viewItem.getConnectivityState()));
holder.enabled.setOnCheckedChangeListener((switch_, checked) -> {
if (!switch_.isPressed()) {
return;
}
viewModel.setAccountEnabled(account, checked);
@ -78,6 +80,12 @@ public class AccountListRecyclerViewAdapter extends RecyclerView.Adapter<Account
holder.mView.setOnClickListener(v -> onAccountClickListener.onAccountListItemClick(account));
}
private static boolean shouldSwitchBeEnabled(ConnectivityState connectivity) {
return connectivity == ConnectivityState.authenticated ||
connectivity == ConnectivityState.disconnected ||
connectivity == ConnectivityState.disconnectedOnError;
}
class ViewHolder extends RecyclerView.ViewHolder {
final View mView;
final ImageView avatar;

View File

@ -0,0 +1,109 @@
package org.mercury_im.messenger.android.ui.account.login;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.crypto.ikey.IkeyBackupCreationFragment;
import org.mercury_im.messenger.android.di.component.AppComponent;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.entity.Account;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
import lombok.Getter;
public class AddAccountActivity extends AppCompatActivity {
@BindView(R.id.viewpager)
ViewPager2 viewPager;
private SetupPagerAdapter pagerAdapter;
private AppComponent appComponent;
private IkeySetupViewModel ikeyViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_viewpager2);
ButterKnife.bind(this);
appComponent = MercuryImApplication.getApplication().getAppComponent();
pagerAdapter = new SetupPagerAdapter(this);
viewPager.setAdapter(pagerAdapter);
viewPager.setUserInputEnabled(false); // disable swiping
ikeyViewModel = new ViewModelProvider(this).get(IkeySetupViewModel.class); // shared between fragments
}
public void loginFinished(Optional<Account> optionalAccount) {
if (optionalAccount.isPresent()) {
pagerAdapter.getFragments().put(1, IkeySetupFragment.newInstance(optionalAccount.getItem().getId()));
pagerAdapter.notifyDataSetChanged();
viewPager.setCurrentItem(1);
}
}
public void skipIkeySetup() {
finish();
}
public void setupIkey(UUID accountId) {
int pos = pagerAdapter.getItemCount();
pagerAdapter.getFragments().put(pos, IkeyBackupRestoreOrSkipFragment.newInstance(accountId));
pagerAdapter.notifyDataSetChanged();
viewPager.setCurrentItem(pos);
}
public void restoreIkeyBackup(UUID accountId) {
int pos = pagerAdapter.getItemCount();
pagerAdapter.getFragments().put(pos, IkeyBackupRestoreSuccessfulFragment.newInstance(accountId));
pagerAdapter.notifyDataSetChanged();
viewPager.setCurrentItem(pos);
}
public void generateIkeyBackup(UUID accountId) {
int pos = pagerAdapter.getItemCount();
pagerAdapter.getFragments().put(pos, IkeyBackupCreationFragment.newInstance(accountId));
pagerAdapter.notifyDataSetChanged();
viewPager.setCurrentItem(pos);
}
private class SetupPagerAdapter extends FragmentStateAdapter {
@Getter
private final Map<Integer, Fragment> fragments = new LinkedHashMap<>();
SetupPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
fragments.put(0, EnterAccountDetailsFragment.newInstance(appComponent));
}
@NonNull
@Override
public Fragment createFragment(int position) {
return fragments.get(position);
}
@Override
public int getItemCount() {
return fragments.size();
}
}
}

View File

@ -44,7 +44,7 @@ public class AddAccountDialogFragment extends AppCompatDialogFragment {
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
LayoutInflater inflater = requireActivity().getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_login, null);
View dialogView = inflater.inflate(R.layout.view_account_credentials, null);
ButterKnife.bind(this, dialogView);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
@ -83,8 +83,8 @@ public class AddAccountDialogFragment extends AppCompatDialogFragment {
viewModel.getLoginPasswordError().observe(this, error -> passwordLayout.setError(error));
viewModel.isLoginButtonEnabled().observe(this, positiveButton::setEnabled);
viewModel.isLoginFinished().observe(this, finished -> {
if (finished) {
viewModel.isLoginFinished().observe(this, optAccount -> {
if (optAccount.isPresent()) {
dismiss();
}
});

View File

@ -14,6 +14,7 @@ import org.mercury_im.messenger.core.account.error.PasswordError;
import org.mercury_im.messenger.core.account.error.UsernameError;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.account.LoginViewModel;
import org.mercury_im.messenger.entity.Account;
import javax.inject.Inject;
@ -24,7 +25,7 @@ public class AndroidLoginViewModel extends AndroidViewModel implements MercuryAn
private final MutableLiveData<String> loginUsernameError = new MutableLiveData<>();
private final MutableLiveData<String> loginPasswordError = new MutableLiveData<>();
private final MutableLiveData<Boolean> loginButtonEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> loginFinished = new MutableLiveData<>(false);
private final MutableLiveData<Optional<Account>> loginFinished = new MutableLiveData<>(new Optional<>());
@Inject
LoginViewModel commonViewModel;
@ -99,8 +100,8 @@ public class AndroidLoginViewModel extends AndroidViewModel implements MercuryAn
getCommonViewModel().dispose();
}
public void onLoginButtonClicked() {
getCommonViewModel().login();
public Account onLoginButtonClicked() {
return getCommonViewModel().login();
}
public LiveData<String> getLoginUsernameError() {
@ -115,7 +116,7 @@ public class AndroidLoginViewModel extends AndroidViewModel implements MercuryAn
return loginButtonEnabled;
}
public LiveData<Boolean> isLoginFinished() {
public LiveData<Optional<Account>> isLoginFinished() {
return loginFinished;
}

View File

@ -0,0 +1,117 @@
package org.mercury_im.messenger.android.ui.account.login;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;
import org.mercury_im.messenger.R;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.di.component.AppComponent;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
public class EnterAccountDetailsFragment extends Fragment {
@BindView(R.id.username_layout)
TextInputLayout usernameLayout;
@BindView(R.id.username)
TextInputEditText username;
@BindView(R.id.password_layout)
TextInputLayout passwordLayout;
@BindView(R.id.password)
TextInputEditText password;
@BindView(R.id.btn_login)
Button loginButton;
@BindView(R.id.btn_cancel)
Button cancelButton;
@BindView(R.id.progress)
ProgressBar progressBar;
@Inject
AndroidLoginViewModel viewModel;
public EnterAccountDetailsFragment(AppComponent appComponent) {
appComponent.inject(this);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_enter_account_credentials, container, false);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new AndroidLoginViewModel(MercuryImApplication.getApplication());
loginButton.setOnClickListener(v -> viewModel.onLoginButtonClicked());
cancelButton.setOnClickListener(v -> getActivity().finish());
viewModel.getLoginUsernameError().observe(this, error -> usernameLayout.setError(error));
viewModel.getLoginPasswordError().observe(this, error -> passwordLayout.setError(error));
viewModel.isLoginButtonEnabled().observe(this, loginButton::setEnabled);
viewModel.isLoginButtonEnabled().observe(this, enabled ->
progressBar.setVisibility(enabled ? View.GONE : View.VISIBLE));
viewModel.isLoginFinished().observe(this, optAccount ->
((AddAccountActivity) getActivity()).loginFinished(optAccount));
username.addTextChangedListener(viewModel.getUsernameTextChangedListener());
password.addTextChangedListener(viewModel.getPasswordTextChangedListener());
username.setOnEditorActionListener(focusPasswordFieldOnEnterPressed);
password.setOnEditorActionListener(loginOnEnterPressed);
}
private final TextView.OnEditorActionListener focusPasswordFieldOnEnterPressed = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_NEXT) {
password.requestFocus();
return true;
}
return false;
}
};
private final TextView.OnEditorActionListener loginOnEnterPressed = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
viewModel.onLoginButtonClicked();
return true;
}
return false;
}
};
public static EnterAccountDetailsFragment newInstance(AppComponent appComponent) {
return new EnterAccountDetailsFragment(appComponent);
}
}

View File

@ -0,0 +1,68 @@
package org.mercury_im.messenger.android.ui.account.login;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import org.mercury_im.messenger.R;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
public class IkeyBackupRestoreOrSkipFragment extends Fragment {
@BindView(R.id.btn_restore)
Button restoreButton;
@BindView(R.id.btn_new_key)
Button regenerateButton;
@BindView(R.id.edit_backup_code)
EditText backupCodeEditText;
@BindView(R.id.btn_scan)
ImageButton scanButton;
private final UUID accountId;
private IkeySetupViewModel viewModel;
IkeyBackupRestoreOrSkipFragment(UUID accountId) {
this.accountId = accountId;
}
public static IkeyBackupRestoreOrSkipFragment newInstance(UUID accountId) {
return new IkeyBackupRestoreOrSkipFragment(accountId);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_ikey_backup_restore, container, false);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(IkeySetupViewModel.class);
restoreButton.setOnClickListener(v ->
((AddAccountActivity) getActivity()).restoreIkeyBackup(accountId));
regenerateButton.setOnClickListener(v ->
((AddAccountActivity) getActivity()).generateIkeyBackup(accountId));
}
}

View File

@ -0,0 +1,56 @@
package org.mercury_im.messenger.android.ui.account.login;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import org.mercury_im.messenger.R;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
public class IkeyBackupRestoreSuccessfulFragment extends Fragment {
@BindView(R.id.btn_done)
Button doneButton;
@BindView(R.id.text)
TextView fingerprint;
private final UUID accountId;
private IkeySetupViewModel viewModel;
IkeyBackupRestoreSuccessfulFragment(UUID accountId) {
this.accountId = accountId;
}
public static IkeyBackupRestoreSuccessfulFragment newInstance(UUID accountId) {
return new IkeyBackupRestoreSuccessfulFragment(accountId);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_ikey_backup_restore_success, container);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(IkeySetupViewModel.class);
doneButton.setOnClickListener(v -> getActivity().finish());
}
}

View File

@ -0,0 +1,74 @@
package org.mercury_im.messenger.android.ui.account.login;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import org.mercury_im.messenger.R;
import java.util.UUID;
import butterknife.BindView;
import butterknife.ButterKnife;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
public class IkeySetupFragment extends Fragment {
@BindView(R.id.btn_skip)
Button skipButton;
@BindView(R.id.btn_continue)
Button continueButton;
private final UUID accountId;
private IkeySetupViewModel viewModel;
public IkeySetupFragment(UUID accountId) {
this.accountId = accountId;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_ikey_setup, container, false);
ButterKnife.bind(this, view);
return view;
}
public static IkeySetupFragment newInstance(UUID accountId) {
return new IkeySetupFragment(accountId);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(IkeySetupViewModel.class);
viewModel.init(accountId);
skipButton.setOnClickListener(v ->
getActivity().finish());
continueButton.setOnClickListener(v -> {
viewModel.fetchBackupElement()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(opt -> {
if (opt.isPresent()) {
((AddAccountActivity) getActivity()).setupIkey(accountId);
} else {
((AddAccountActivity) getActivity()).generateIkeyBackup(accountId);
}
})
.subscribe();
});
}
}

View File

@ -0,0 +1,73 @@
package org.mercury_im.messenger.android.ui.account.login;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smackx.ikey.IkeyManager;
import org.jivesoftware.smackx.ox.element.SecretkeyElement;
import org.jivesoftware.smackx.pubsub.PubSubException;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.crypto.ikey.IkeyInitializer;
import org.mercury_im.messenger.core.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import io.reactivex.Maybe;
import io.reactivex.MaybeObserver;
import io.reactivex.Single;
public class IkeySetupViewModel extends AndroidViewModel {
@Inject
MercuryConnectionManager connectionManager;
@Inject
IkeyInitializer ikeyInitializer;
IkeyManager ikeyManager;
private SecretkeyElement secretkeyElement;
public IkeySetupViewModel(@NonNull Application application) {
super(application);
MercuryImApplication.getApplication().getAppComponent().inject(this);
}
public void init(UUID accountId) {
MercuryConnection connection = connectionManager.getConnection(accountId);
ikeyManager = ikeyInitializer.initFor(connection);
}
public Single<Optional<SecretkeyElement>> fetchBackupElement() {
return fetchMaybeBackupElement()
.map(Optional::new)
.toSingle(new Optional<>());
}
private Maybe<SecretkeyElement> fetchMaybeBackupElement() {
return new Maybe<SecretkeyElement>() {
@Override
protected void subscribeActual(MaybeObserver<? super SecretkeyElement> observer) {
try {
secretkeyElement = ikeyManager.fetchSecretIdentityKey();
if (secretkeyElement != null) {
observer.onSuccess(secretkeyElement);
} else {
observer.onComplete();
}
} catch (PubSubException.NotALeafNodeException e) {
observer.onComplete();
} catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException e) {
observer.onError(e);
}
}
};
}
}

View File

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import org.mercury_im.messenger.android.MercuryImApplication;
import org.mercury_im.messenger.android.ui.MercuryAndroidViewModel;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.openpgp.OxBackupRestoreError;
@ -14,12 +15,14 @@ import org.mercury_im.messenger.core.viewmodel.openpgp.OxSecretKeyBackupRestoreV
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
public class AndroidOxSecretKeyBackupRestoreViewModel extends AndroidViewModel
implements MercuryAndroidViewModel<OxSecretKeyBackupRestoreViewModel> {
private static final Logger LOGGER = Logger.getLogger(AndroidOxSecretKeyBackupRestoreViewModel.class.getName());
//@Inject
// @Inject
OxSecretKeyBackupRestoreViewModel commonViewModel;
private MutableLiveData<Optional<OxBackupRestoreError>> restoreError =
@ -27,7 +30,7 @@ public class AndroidOxSecretKeyBackupRestoreViewModel extends AndroidViewModel
public AndroidOxSecretKeyBackupRestoreViewModel(@NonNull Application application) {
super(application);
//MercuryImApplication.getApplication().getAppComponent().inject(this);
// MercuryImApplication.getApplication().getAppComponent().inject(this);
addDisposable(getCommonViewModel().observeBackupRestoreError()
.subscribe(opt -> restoreError.postValue(opt),

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/view_account_credentials">
</include>
<ProgressBar android:id="@+id/progress"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_centerInParent="true"
android:visibility="gone"/>
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancel" />
<Button
android:id="@+id/btn_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login" />
</LinearLayout>
</LinearLayout>

View File

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.ui.ikey.IkeyBackupCreationFragment">
tools:context=".android.crypto.ikey.IkeyBackupCreationFragment">
<TextView
android:id="@+id/notice"
@ -17,12 +17,14 @@
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="You should now write down the code below and store it somewhere safe.\nAlternatively you can scan the code below with another device to copy the identity key over."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notice" />
app:layout_constraintTop_toBottomOf="@+id/notice"
android:gravity="center"/>
<ImageView
android:id="@+id/qr_code"
@ -32,7 +34,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notice"
app:layout_constraintTop_toBottomOf="@+id/description"
app:srcCompat="@drawable/ic_qr_code_scanner_black_24dp" />
<TextView

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.ui.ikey.IkeyBackupRestoreActivity">
android:orientation="vertical"
tools:context=".android.crypto.ikey.IkeyBackupRestoreActivity">
<TextView
android:id="@+id/notice"
@ -16,22 +17,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/notice">
<include
layout="@layout/view_openpgp_4_fingerprint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
tools:layout_editor_absoluteY="19dp" />
</LinearLayout>
<TextView
android:id="@+id/textView"
@ -40,8 +25,7 @@
android:text="In order to restore the backup, please enter the backup code below."
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout" />
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -54,17 +38,18 @@
app:layout_constraintTop_toBottomOf="@+id/textView" >
<EditText
android:id="@+id/edit_backup_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Backup Code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/imageButton"
app:layout_constraintEnd_toStartOf="@+id/btn_scan"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginEnd="8dp"/>
<ImageButton
android:id="@+id/imageButton"
android:id="@+id/btn_scan"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@android:color/transparent"
@ -77,4 +62,24 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_restore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Restore"/>
<Button
android:id="@+id/btn_new_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Generate new key"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Backup restored successfully!\nThis is your Identity Key Fingerprint:"
android:gravity="center"
/>
<include layout="@layout/view_openpgp_4_fingerprint" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This key will help your contacts to decide which keys really belong to you."
android:gravity="center"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Done" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Setup Simplified Encryption Key Management?"
android:gravity="center"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Ikey simplifies management of encryption keys when multiple devices are in use."
android:gravity="center"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_skip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Skip" />
<Button
android:id="@+id/btn_continue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Yes, please" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -5,7 +5,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/login_form"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/toggle_account_enabled"
android:id="@+id/switch_account_enabled"
android:title="Enable Account"
app:actionLayout="@layout/switch_item"
app:showAsAction="always" />

View File

@ -99,6 +99,15 @@ public class MercuryConnectionManager {
return connectionsMap.get(id);
}
public MercuryConnection createConnection(UUID accountId) {
Account account = accountRepository.getAccount(accountId)
.blockingGet();
if (account == null) {
throw new IllegalArgumentException("No account associated with this accountID: " + accountId);
}
return createConnection(account);
}
public MercuryConnection createConnection(Account account) {
return new MercuryConnection(connectionFactory.createConnection(account), account);
}

View File

@ -0,0 +1,6 @@
package org.mercury_im.messenger.core.crypto;
public interface LocalOxKeyGenerationStrategy {
boolean promptForBackupRestoreIfNoLocalKeyPresent();
}

View File

@ -4,6 +4,7 @@ import org.jivesoftware.smack.AbstractConnectionListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.jivesoftware.smackx.ox.OpenPgpSecretKeyBackupPassphrase;
import org.jivesoftware.smackx.ox.callback.SecretKeyPassphraseCallback;
import org.jivesoftware.smackx.ox.crypto.OpenPgpProvider;
import org.jivesoftware.smackx.ox.crypto.PainlessOpenPgpProvider;
import org.jivesoftware.smackx.ox.exception.InvalidBackupCodeException;
@ -35,6 +36,7 @@ public class MercuryOpenPgpManager {
private final OpenPgpRepository openPgpRepository;
private final SchedulersFacade schedulers;
private final OpenPgpSecretKeyBackupPassphraseGenerator passphraseGenerator;
private final LocalOxKeyGenerationStrategy keyGenerationStrategy;
@Inject
public MercuryOpenPgpManager(PeerRepository peerRepository,
@ -42,12 +44,14 @@ public class MercuryOpenPgpManager {
MessageRepository messageRepository,
OpenPgpRepository openPgpRepository,
OpenPgpSecretKeyBackupPassphraseGenerator passphraseGenerator,
LocalOxKeyGenerationStrategy keyGenerationStrategy,
SchedulersFacade schedulers) {
this.peerRepository = peerRepository;
this.directChatRepository = directChatRepository;
this.messageRepository = messageRepository;
this.openPgpRepository = openPgpRepository;
this.schedulers = schedulers;
this.keyGenerationStrategy = keyGenerationStrategy;
this.passphraseGenerator = passphraseGenerator;
}
@ -72,8 +76,8 @@ public class MercuryOpenPgpManager {
OpenPgpManager oxManager = OpenPgpManager.getInstanceFor(connection.getConnection());
oxManager.setOpenPgpProvider(provider);
OpenPgpSecretKeyBackupPassphrase passphrase = passphraseGenerator.generateBackupPassphrase();
boolean mustGenerate = false;
try {
boolean mustGenerate = false;
if (!oxManager.hasSecretKeysAvailable()) {
mustGenerate = true;
if (OpenPgpManager.serverSupportsSecretKeyBackups(connection.getConnection())) {
@ -105,4 +109,8 @@ public class MercuryOpenPgpManager {
e.printStackTrace();
}
}
public boolean mustPromptForRestore() {
return keyGenerationStrategy.promptForBackupRestoreIfNoLocalKeyPresent();
}
}

View File

@ -0,0 +1,9 @@
package org.mercury_im.messenger.core.crypto;
public class OxPlusIkeyKeyGenerationStrategy implements LocalOxKeyGenerationStrategy {
@Override
public boolean promptForBackupRestoreIfNoLocalKeyPresent() {
return false;
}
}

View File

@ -0,0 +1,10 @@
package org.mercury_im.messenger.core.di.component;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@java.lang.annotation.Documented
@java.lang.annotation.Retention(RUNTIME)
@javax.inject.Qualifier
public @interface Account {
}

View File

@ -0,0 +1,21 @@
package org.mercury_im.messenger.core.di.component;
import java.util.UUID;
import dagger.BindsInstance;
import dagger.Component;
@Component
public interface ConnectionComponent {
ConnectionComponent getComponent();
@Component.Builder
interface Builder {
@BindsInstance Builder forAccount(@Account UUID accountId);
ConnectionComponent build();
}
}

View File

@ -0,0 +1,19 @@
package org.mercury_im.messenger.core.di.module;
import org.mercury_im.messenger.core.crypto.LocalOxKeyGenerationStrategy;
import org.mercury_im.messenger.core.crypto.OxPlusIkeyKeyGenerationStrategy;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
@Module
public class IkeyModule {
@Singleton
@Provides
static LocalOxKeyGenerationStrategy provideLocalOxKeyGenerationStrategy() {
return new OxPlusIkeyKeyGenerationStrategy();
}
}

View File

@ -0,0 +1,39 @@
package org.mercury_im.messenger.core.di.module;
import org.jivesoftware.smackx.ikey.IkeyManager;
import org.jivesoftware.smackx.ox.OpenPgpManager;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.di.component.Account;
import java.util.UUID;
import dagger.Module;
import dagger.Provides;
@Module
public class MercuryConnectionModule {
@Provides
@Account
MercuryConnection provideConnection(MercuryConnectionManager connectionManager, @Account UUID accountId) {
MercuryConnection connection = connectionManager.getConnection(accountId);
if (connection == null) {
connection = connectionManager.createConnection(accountId);
connectionManager.doRegisterConnection(connection);
}
return connection;
}
@Provides
@Account
OpenPgpManager provideOpenPgpManager(MercuryConnection connection) {
return OpenPgpManager.getInstanceFor(connection.getConnection());
}
@Provides
@Account
IkeyManager provideIkeyManager(MercuryConnection connection) {
return IkeyManager.getInstanceFor(connection.getConnection());
}
}

View File

@ -1,7 +1,9 @@
package org.mercury_im.messenger.core.di.module;
import org.mercury_im.messenger.core.crypto.InsecureStaticSecretKeyBackupPassphraseGenerator;
import org.mercury_im.messenger.core.crypto.LocalOxKeyGenerationStrategy;
import org.mercury_im.messenger.core.crypto.OpenPgpSecretKeyBackupPassphraseGenerator;
import org.mercury_im.messenger.core.crypto.OxPlusIkeyKeyGenerationStrategy;
import javax.inject.Singleton;
@ -17,5 +19,4 @@ public class OpenPgpModule {
// TODO: THIS MUST NEVER MAKE IT TO PRODUCTION!!!
return new InsecureStaticSecretKeyBackupPassphraseGenerator();
}
}

View File

@ -7,15 +7,17 @@ import org.jxmpp.stringprep.XmppStringprepException;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.account.error.PasswordError;
import org.mercury_im.messenger.core.account.error.UsernameError;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.connection.exception.InvalidCredentialsException;
import org.mercury_im.messenger.core.connection.exception.ServerUnreachableException;
import org.mercury_im.messenger.core.connection.state.ConnectionState;
import org.mercury_im.messenger.core.connection.state.ConnectivityState;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
import org.mercury_im.messenger.core.util.Optional;
import org.mercury_im.messenger.core.viewmodel.MercuryViewModel;
import org.mercury_im.messenger.entity.Account;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
@ -35,10 +37,11 @@ public class LoginViewModel implements MercuryViewModel {
private BehaviorSubject<Optional<UsernameError>> loginUsernameError;
private BehaviorSubject<Optional<PasswordError>> loginPasswordError;
private BehaviorSubject<Boolean> isLoginPossible;
private BehaviorSubject<Boolean> isLoginSuccessful;
private BehaviorSubject<Optional<Account>> isLoginSuccessful;
private EntityBareJid loginUsernameValue;
private String loginPasswordValue;
private Account account;
@Inject
public LoginViewModel(MercuryConnectionManager connectionManager,
@ -55,7 +58,7 @@ public class LoginViewModel implements MercuryViewModel {
loginUsernameError = BehaviorSubject.createDefault(new Optional<>());
loginPasswordError = BehaviorSubject.createDefault(new Optional<>());
isLoginPossible = BehaviorSubject.createDefault(false);
isLoginSuccessful = BehaviorSubject.createDefault(false);
isLoginSuccessful = BehaviorSubject.createDefault(new Optional<>());
loginUsernameValue = null;
loginPasswordValue = null;
@ -73,7 +76,7 @@ public class LoginViewModel implements MercuryViewModel {
return isLoginPossible;
}
public Observable<Boolean> isLoginSuccessful() {
public Observable<Optional<Account>> isLoginSuccessful() {
return isLoginSuccessful;
}
@ -108,26 +111,48 @@ public class LoginViewModel implements MercuryViewModel {
isLoginPossible.onNext(loginUsernameValue != null && loginPasswordValue != null);
}
public synchronized void login() {
public synchronized Account login() {
if (!isLoginPossible.getValue()) {
// Prevent race condition where account would be logged in twice
return;
return account;
}
isLoginPossible.onNext(false);
Account account = createAccountEntity();
account = createAccountEntity();
//MercuryConnection connection = connectionManager.createConnection(account);
addDisposable(accountRepository.upsertAccount(account).ignoreElement()
addDisposable(accountRepository.upsertAccount(account)
//.andThen(connection.connect())
//.andThen(connection.login())
//.andThen(connectionManager.registerConnection(connection))
.subscribeOn(schedulers.getNewThread())
.observeOn(schedulers.getUiScheduler())
.subscribe(
this::onLoginSuccessful,
a -> LOGGER.log(Level.INFO, "Account " + a + " successfully inserted."),
this::onLoginFailed
));
addDisposable(connectionManager.observeConnectionPool()
.map(cp -> {
ConnectionState state = cp.getConnectionStates().get(account.getId());
if (state == null) {
return ConnectivityState.disconnected;
} else {
return state.getConnectivity();
}
})
.filter(s -> s == ConnectivityState.disconnectedOnError || s == ConnectivityState.authenticated)
.firstOrError()
.compose(schedulers.executeUiSafeSingle())
.subscribe(connectivityState -> {
if (connectivityState == ConnectivityState.disconnectedOnError) {
onLoginFailed(new Exception());
} else {
onLoginSuccessful(account);
}
}));
return account;
}
private Account createAccountEntity() {
@ -139,11 +164,13 @@ public class LoginViewModel implements MercuryViewModel {
return account;
}
private void onLoginSuccessful() {
isLoginSuccessful.onNext(true);
private void onLoginSuccessful(Account account) {
LOGGER.log(Level.INFO, "Login successful.");
isLoginSuccessful.onNext(new Optional<>(account));
}
private void onLoginFailed(Throwable error) {
LOGGER.log(Level.INFO, "Login failed!");
isLoginPossible.onNext(true);
if (error instanceof InvalidCredentialsException) {
loginPasswordError.onNext(new Optional<>(PasswordError.incorrectPassword));

View File

@ -14,6 +14,8 @@ import org.jxmpp.jid.EntityBareJid;
import org.mercury_im.messenger.core.SchedulersFacade;
import org.mercury_im.messenger.core.connection.MercuryConnection;
import org.mercury_im.messenger.core.connection.MercuryConnectionManager;
import org.mercury_im.messenger.core.connection.state.ConnectionState;
import org.mercury_im.messenger.core.connection.state.ConnectivityState;
import org.mercury_im.messenger.core.crypto.ikey.IkeyInitializer;
import org.mercury_im.messenger.core.crypto.ikey.IkeyRepository;
import org.mercury_im.messenger.core.data.repository.AccountRepository;
@ -27,15 +29,12 @@ import org.pgpainless.key.OpenPgpV4Fingerprint;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.disposables.Disposable;
import static org.jivesoftware.smackx.ox.util.OpenPgpPubSubUtil.PEP_NODE_PUBLIC_KEYS;
@ -155,4 +154,18 @@ public class AccountDetailsViewModel implements MercuryViewModel {
SecretkeyElement secretkeyElement = ikeyManager.fetchSecretIdentityKey();
});
}
public Observable<Boolean> isAccountEnabled(UUID accountId) {
return accountRepository.observeAccount(accountId)
.filter(Optional::isPresent)
.map(Optional::getItem)
.map(Account::isEnabled);
}
public Observable<Boolean> isAccountAuthenticated(UUID accountId) {
return connectionManager.getConnection(accountId)
.observeConnection()
.map(ConnectionState::getConnectivity)
.map(connectivity -> connectivity == ConnectivityState.authenticated);
}
}

View File

@ -31,7 +31,7 @@ public class OxSecretKeyBackupRestoreViewModel implements MercuryViewModel {
private BehaviorSubject<Optional<OxBackupRestoreError>> backupRestoreError = BehaviorSubject.createDefault(new Optional<>());
private BehaviorSubject<Boolean> finished = BehaviorSubject.createDefault(false);
@Inject
//@Inject
public OxSecretKeyBackupRestoreViewModel(OpenPgpManager openPgpManager) {
this.openPgpManager = openPgpManager;
}