diff --git a/briar-android/src/main/java/org/briarproject/briar/android/account/SetupViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/account/SetupViewModel.java index 322d4fd24..845075e8d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/account/SetupViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/account/SetupViewModel.java @@ -29,6 +29,7 @@ import static org.briarproject.briar.android.account.SetupViewModel.State.SET_PA @MethodsNotNullByDefault @ParametersNotNullByDefault +public class SetupViewModel extends AndroidViewModel { enum State {AUTHOR_NAME, SET_PASSWORD, DOZE, CREATED, FAILED} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index b97ee15ff..a3b587a0d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -95,6 +95,7 @@ import org.briarproject.briar.android.socialbackup.ShardsSentFragment; import org.briarproject.briar.android.socialbackup.ThresholdSelectorFragment; import org.briarproject.briar.android.socialbackup.creation.CreateBackupModule; import org.briarproject.briar.android.socialbackup.recover.RestoreAccountActivity; +import org.briarproject.briar.android.socialbackup.recover.RestoreAccountSetPasswordFragment; import org.briarproject.briar.android.splash.SplashScreenActivity; import org.briarproject.briar.android.test.TestDataActivity; import org.briarproject.briar.api.socialbackup.recovery.RestoreAccount; @@ -291,4 +292,6 @@ public interface ActivityComponent { void inject(OwnerReturnShardFragment ownerReturnShardFragment); void inject(CustodianReturnShardSuccessFragment custodianReturnShardSuccessFragment); + + void inject(RestoreAccountSetPasswordFragment restoreAccountSetPasswordFragment); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java index b2f2da6e4..a68e587a6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/OwnerReturnShardModule.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android.socialbackup.recover; import org.briarproject.briar.android.viewmodel.ViewModelKey; +import org.briarproject.briar.api.socialbackup.recovery.RestoreAccount; import androidx.lifecycle.ViewModel; import dagger.Binds; @@ -17,4 +18,9 @@ public abstract class OwnerReturnShardModule { abstract ViewModel bindOwnerReturnShardViewModel( OwnerReturnShardViewModel ownerReturnShardViewModel); + @Binds + @IntoMap + @ViewModelKey(RestoreAccountViewModel.class) + abstract ViewModel bindRestoreAccountViewModel( + RestoreAccountViewModel restoreAccountViewModel); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountActivity.java index 5031e0855..e63ed26ae 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountActivity.java @@ -8,7 +8,6 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.account.DozeFragment; -import org.briarproject.briar.android.account.SetPasswordFragment; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BaseActivity; import org.briarproject.briar.android.fragment.BaseFragment; @@ -50,10 +49,10 @@ public class RestoreAccountActivity extends BaseActivity } private void onStateChanged(RestoreAccountViewModel.State state) { - if (state == RestoreAccountViewModel.State.SET_PASSWORD) { - showInitialFragment(SetPasswordFragment.newInstance()); + if (state == State.SET_PASSWORD) { + showInitialFragment(RestoreAccountSetPasswordFragment.newInstance()); } else if (state == State.DOZE) { - showDozeFragment(); +// showDozeFragment(); } else if (state == State.CREATED || state == State.FAILED) { // TODO: Show an error if failed showApp(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountFragment.java new file mode 100644 index 000000000..329df7ee2 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountFragment.java @@ -0,0 +1,91 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.fragment.BaseFragment; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; +import static android.view.inputmethod.EditorInfo.IME_ACTION_NEXT; +import static org.briarproject.briar.android.util.UiUtils.enterPressed; +import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +abstract class RestoreAccountFragment extends BaseFragment implements TextWatcher, + TextView.OnEditorActionListener, View.OnClickListener { + + private final static String STATE_KEY_CLICKED = "setupFragmentClicked"; + + @Inject + ViewModelProvider.Factory viewModelFactory; + RestoreAccountViewModel viewModel; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(requireActivity()) + .get(RestoreAccountViewModel.class); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.help_action, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_help) { + showOnboardingDialog(getContext(), getHelpText()); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + protected abstract String getHelpText(); + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + // noop + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + // noop + } + + @Override + public boolean onEditorAction(TextView textView, int actionId, + @Nullable KeyEvent keyEvent) { + if (actionId == IME_ACTION_NEXT || actionId == IME_ACTION_DONE || + enterPressed(actionId, keyEvent)) { + onClick(textView); + return true; + } + return false; + } + + @Override + public void afterTextChanged(Editable editable) { + // noop + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountSetPasswordFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountSetPasswordFragment.java new file mode 100644 index 000000000..c75b3bbed --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/socialbackup/recover/RestoreAccountSetPasswordFragment.java @@ -0,0 +1,131 @@ +package org.briarproject.briar.android.socialbackup.recover; + +import android.os.Bundle; +import android.os.IBinder; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.ProgressBar; + +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.login.StrengthMeter; + +import javax.annotation.Nullable; + +import static android.content.Context.INPUT_METHOD_SERVICE; +import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; +import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; +import static org.briarproject.bramble.api.crypto.PasswordStrengthEstimator.QUITE_WEAK; +import static org.briarproject.briar.android.util.UiUtils.setError; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RestoreAccountSetPasswordFragment extends RestoreAccountFragment { + private final static String TAG = RestoreAccountSetPasswordFragment.class.getName(); + + private TextInputLayout passwordEntryWrapper; + private TextInputLayout passwordConfirmationWrapper; + private TextInputEditText passwordEntry; + private TextInputEditText passwordConfirmation; + private StrengthMeter strengthMeter; + private Button nextButton; + + public static RestoreAccountSetPasswordFragment newInstance() { + return new RestoreAccountSetPasswordFragment(); + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + requireActivity().setTitle(getString(R.string.setup_password_intro)); + View v = inflater.inflate(R.layout.fragment_setup_password, container, + false); + + strengthMeter = v.findViewById(R.id.strength_meter); + passwordEntryWrapper = v.findViewById(R.id.password_entry_wrapper); + passwordEntry = v.findViewById(R.id.password_entry); + passwordConfirmationWrapper = + v.findViewById(R.id.password_confirm_wrapper); + passwordConfirmation = v.findViewById(R.id.password_confirm); + nextButton = v.findViewById(R.id.next); + ProgressBar progressBar = v.findViewById(R.id.progress); + + passwordEntry.addTextChangedListener(this); + passwordConfirmation.addTextChangedListener(this); + nextButton.setOnClickListener(this); + + if (!viewModel.needToShowDozeFragment()) { + nextButton.setText(R.string.create_account_button); + passwordConfirmation.setImeOptions(IME_ACTION_DONE); + } + + viewModel.getIsCreatingAccount() + .observe(getViewLifecycleOwner(), isCreatingAccount -> { + if (isCreatingAccount) { + nextButton.setVisibility(INVISIBLE); + progressBar.setVisibility(VISIBLE); + // this also avoids the keyboard popping up + passwordEntry.setFocusable(false); + passwordConfirmation.setFocusable(false); + } + }); + + return v; + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + protected String getHelpText() { + return getString(R.string.setup_password_explanation); + } + + @Override + public void onTextChanged(CharSequence authorName, int i, int i1, int i2) { + String password1 = passwordEntry.getText().toString(); + String password2 = passwordConfirmation.getText().toString(); + boolean passwordsMatch = password1.equals(password2); + + strengthMeter + .setVisibility(password1.length() > 0 ? VISIBLE : INVISIBLE); + float strength = viewModel.estimatePasswordStrength(password1); + strengthMeter.setStrength(strength); + boolean strongEnough = strength >= QUITE_WEAK; + + setError(passwordEntryWrapper, getString(R.string.password_too_weak), + password1.length() > 0 && !strongEnough); + setError(passwordConfirmationWrapper, + getString(R.string.passwords_do_not_match), + password2.length() > 0 && !passwordsMatch); + + boolean enabled = passwordsMatch && strongEnough; + nextButton.setEnabled(enabled); + passwordConfirmation.setOnEditorActionListener(enabled ? this : null); + } + + @Override + public void onClick(View view) { + IBinder token = passwordEntry.getWindowToken(); + Object o = getContext().getSystemService(INPUT_METHOD_SERVICE); + ((InputMethodManager) o).hideSoftInputFromWindow(token, 0); + viewModel.setPassword(passwordEntry.getText().toString()); + } +}