From fb82420376c13190be20dd301acb4eecec87d631 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Thu, 30 Jan 2020 16:23:29 -0400 Subject: [PATCH] Implement new PIN UX. --- app/src/main/AndroidManifest.xml | 18 +- .../securesms/ApplicationContext.java | 2 + .../PassphraseRequiredActionBarActivity.java | 37 +++ .../animation/AnimationRepeatListener.java | 32 +++ .../ConversationListFragment.java | 56 +++-- .../ConversationListViewModel.java | 14 +- .../securesms/keyvalue/KbsValues.java | 15 ++ .../securesms/keyvalue/KeyValueDataSet.java | 8 +- .../keyvalue/RegistrationValues.java | 43 ++++ .../securesms/keyvalue/SignalStore.java | 4 + .../lock/RegistrationLockDialog.java | 180 +++++++++++++- .../lock/RegistrationLockReminders.java | 20 +- .../securesms/lock/v2/BaseKbsPinFragment.java | 149 ++++++++++++ .../lock/v2/BaseKbsPinViewModel.java | 22 ++ .../lock/v2/ConfirmKbsPinFragment.java | 186 +++++++++++++++ .../lock/v2/ConfirmKbsPinRepository.java | 75 ++++++ .../lock/v2/ConfirmKbsPinViewModel.java | 130 +++++++++++ .../lock/v2/CreateKbsPinActivity.java | 75 ++++++ .../lock/v2/CreateKbsPinFragment.java | 70 ++++++ .../lock/v2/CreateKbsPinViewModel.java | 69 ++++++ .../securesms/lock/v2/KbsConstants.java | 13 ++ .../securesms/lock/v2/KbsKeyboardType.java | 33 +++ .../lock/v2/KbsMigrationActivity.java | 56 +++++ .../securesms/lock/v2/KbsPin.java | 63 +++++ .../securesms/lock/v2/KbsSplashFragment.java | 87 +++++++ .../securesms/lock/v2/PinUtil.java | 18 ++ .../megaphone/BasicMegaphoneView.java | 12 +- .../securesms/megaphone/Megaphone.java | 80 +++---- .../megaphone/MegaphoneListener.java | 7 +- .../megaphone/MegaphoneRepository.java | 13 +- .../securesms/megaphone/Megaphones.java | 75 +++++- .../megaphone/PinsForAllSchedule.java | 48 ++++ .../AdvancedPreferenceFragment.java | 2 + .../AppProtectionPreferenceFragment.java | 42 +++- .../profiles/edit/EditProfileFragment.java | 4 + .../fragments/AccountLockedFragment.java | 59 +++++ .../fragments/EnterCodeFragment.java | 10 +- .../fragments/KbsLockFragment.java | 221 ++++++++++++++++++ .../RegistrationCompleteFragment.java | 17 +- .../securesms/util/FeatureFlags.java | 9 +- .../securesms/util/RequestCodes.java | 8 + .../securesms/util/TextSecurePreferences.java | 3 + .../org/thoughtcrime/securesms/util/Util.java | 4 + .../views/SlideUpWithSnackbarBehavior.java | 45 ++++ .../res/drawable-mdpi/kbs_pin_megaphone.webp | Bin 0 -> 2112 bytes .../res/drawable-xhdpi/kbs_pin_megaphone.webp | Bin 0 -> 6964 bytes .../drawable-xxhdpi/kbs_pin_megaphone.webp | Bin 0 -> 13464 bytes .../drawable-xxxhdpi/kbs_pin_megaphone.webp | Bin 0 -> 22770 bytes .../res/drawable/ic_kbs_splash_dark_svg.xml | 21 ++ .../res/drawable/ic_kbs_splash_light_svg.xml | 44 ++++ .../res/layout/account_locked_fragment.xml | 63 +++++ .../main/res/layout/base_kbs_pin_fragment.xml | 110 +++++++++ .../res/layout/conversation_list_fragment.xml | 97 ++++---- .../res/layout/create_kbs_pin_activity.xml | 17 ++ app/src/main/res/layout/kbs_lock_fragment.xml | 95 ++++++++ .../res/layout/kbs_migration_activity.xml | 17 ++ .../res/layout/kbs_pin_change_preference.xml | 10 + .../main/res/layout/kbs_pin_reminder_view.xml | 79 +++++++ .../main/res/layout/kbs_splash_fragment.xml | 74 ++++++ .../main/res/navigation/create_kbs_pin.xml | 54 +++++ app/src/main/res/navigation/kbs_migration.xml | 26 +++ app/src/main/res/navigation/registration.xml | 53 +++++ app/src/main/res/raw/lottie_kbs_failure.json | 1 + app/src/main/res/raw/lottie_kbs_loading.json | 1 + app/src/main/res/raw/lottie_kbs_success.json | 1 + app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/strings.xml | 35 ++- app/src/main/res/values/text_styles.xml | 6 + app/src/main/res/values/themes.xml | 13 ++ .../res/xml/preferences_app_protection.xml | 9 +- .../megaphone/PinsForAllScheduleTest.java | 219 +++++++++++++++++ 71 files changed, 2989 insertions(+), 192 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/animation/AnimationRepeatListener.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinRepository.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/KbsConstants.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/KbsKeyboardType.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/KbsMigrationActivity.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/KbsPin.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/KbsSplashFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/lock/v2/PinUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/fragments/KbsLockFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/RequestCodes.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/views/SlideUpWithSnackbarBehavior.java create mode 100644 app/src/main/res/drawable-mdpi/kbs_pin_megaphone.webp create mode 100644 app/src/main/res/drawable-xhdpi/kbs_pin_megaphone.webp create mode 100644 app/src/main/res/drawable-xxhdpi/kbs_pin_megaphone.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/kbs_pin_megaphone.webp create mode 100644 app/src/main/res/drawable/ic_kbs_splash_dark_svg.xml create mode 100644 app/src/main/res/drawable/ic_kbs_splash_light_svg.xml create mode 100644 app/src/main/res/layout/account_locked_fragment.xml create mode 100644 app/src/main/res/layout/base_kbs_pin_fragment.xml create mode 100644 app/src/main/res/layout/create_kbs_pin_activity.xml create mode 100644 app/src/main/res/layout/kbs_lock_fragment.xml create mode 100644 app/src/main/res/layout/kbs_migration_activity.xml create mode 100644 app/src/main/res/layout/kbs_pin_change_preference.xml create mode 100644 app/src/main/res/layout/kbs_pin_reminder_view.xml create mode 100644 app/src/main/res/layout/kbs_splash_fragment.xml create mode 100644 app/src/main/res/navigation/create_kbs_pin.xml create mode 100644 app/src/main/res/navigation/kbs_migration.xml create mode 100644 app/src/main/res/raw/lottie_kbs_failure.json create mode 100644 app/src/main/res/raw/lottie_kbs_loading.json create mode 100644 app/src/main/res/raw/lottie_kbs_success.json create mode 100644 app/src/test/java/org/thoughtcrime/securesms/megaphone/PinsForAllScheduleTest.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e948988661..c054a91e59e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -414,11 +414,21 @@ android:windowSoftInputMode="stateVisible" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + + + + android:theme="@style/Theme.AppCompat.Dialog.Alert" + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" + android:icon="@drawable/clear_profile_avatar" + android:label="@string/AndroidManifest_remove_photo"> diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index c554cfa1eab..3da01a9ee97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; @@ -250,6 +251,7 @@ private void initializeFirstEverAppLaunch() { TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode()); TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true); ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch(); + SignalStore.registrationValues().onNewInstall(); ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false)); ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java index 5036cff21c2..b5c9db1a727 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -14,9 +14,14 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; +import org.thoughtcrime.securesms.lock.v2.PinUtil; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; +import org.thoughtcrime.securesms.profiles.ProfileName; +import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -35,6 +40,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA private static final int STATE_UI_BLOCKING_UPGRADE = 3; private static final int STATE_EXPERIENCE_UPGRADE = 4; private static final int STATE_WELCOME_PUSH_SCREEN = 5; + private static final int STATE_CREATE_PROFILE_NAME = 6; + private static final int STATE_CREATE_KBS_PIN = 7; private SignalServiceNetworkAccess networkAccess; private BroadcastReceiver clearKeyReceiver; @@ -150,6 +157,8 @@ private Intent getIntentForState(int state) { case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent(); case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent(); case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent(); + case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent(); + case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent(); default: return null; } } @@ -165,11 +174,23 @@ private int getApplicationState(boolean locked) { return STATE_WELCOME_PUSH_SCREEN; } else if (ExperienceUpgradeActivity.isUpdate(this)) { return STATE_EXPERIENCE_UPGRADE; + } else if (userMustSetKbsPin()) { + return STATE_CREATE_KBS_PIN; + } else if (userMustSetProfileName()) { + return STATE_CREATE_PROFILE_NAME; } else { return STATE_NORMAL; } } + private boolean userMustSetKbsPin() { + return !SignalStore.registrationValues().isRegistrationComplete() && !PinUtil.userHasPin(this); + } + + private boolean userMustSetProfileName() { + return !SignalStore.registrationValues().isRegistrationComplete() && TextSecurePreferences.getProfileName(this) == ProfileName.EMPTY; + } + private Intent getCreatePassphraseIntent() { return getRoutedIntent(PassphraseCreateActivity.class, getIntent()); } @@ -193,6 +214,22 @@ private Intent getPushRegistrationIntent() { return RegistrationNavigationActivity.newIntentForNewRegistration(this); } + private Intent getCreateKbsPinIntent() { + + final Intent intent; + if (userMustSetProfileName()) { + intent = getCreateProfileNameIntent(); + } else { + intent = getIntent(); + } + + return getRoutedIntent(CreateKbsPinActivity.class, intent); + } + + private Intent getCreateProfileNameIntent() { + return getRoutedIntent(EditProfileActivity.class, getIntent()); + } + private Intent getRoutedIntent(Class destination, @Nullable Intent nextIntent) { final Intent intent = new Intent(this, destination); if (nextIntent != null) intent.putExtra("next_intent", nextIntent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationRepeatListener.java b/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationRepeatListener.java new file mode 100644 index 00000000000..28317e9a498 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationRepeatListener.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.animation; + +import android.animation.Animator; + +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; + +public final class AnimationRepeatListener implements Animator.AnimatorListener { + + private final Consumer animationConsumer; + + public AnimationRepeatListener(@NonNull Consumer animationConsumer) { + this.animationConsumer = animationConsumer; + } + + @Override + public final void onAnimationStart(Animator animation) { + } + + @Override + public final void onAnimationEnd(Animator animation) { + } + + @Override + public final void onAnimationCancel(Animator animation) { + } + + @Override + public final void onAnimationRepeat(Animator animation) { + this.animationConsumer.accept(animation); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 8a4ead88e12..d80bbe7b27b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -33,18 +33,26 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; - -import com.google.android.material.snackbar.Snackbar; - import androidx.annotation.PluralsRes; -import androidx.annotation.StringRes; import androidx.annotation.WorkerThread; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.TooltipCompat; import androidx.lifecycle.DefaultLifecycleObserver; @@ -53,22 +61,11 @@ import androidx.lifecycle.ViewModelProviders; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.view.ActionMode; +import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.ItemTouchHelper; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; +import com.google.android.material.snackbar.Snackbar; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -78,7 +75,6 @@ import org.thoughtcrime.securesms.MainNavigator; import org.thoughtcrime.securesms.NewConversationActivity; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.components.RatingManager; import org.thoughtcrime.securesms.components.SearchToolbar; import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator; @@ -94,6 +90,7 @@ import org.thoughtcrime.securesms.components.reminder.ShareReminder; import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; +import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener; import org.thoughtcrime.securesms.conversationlist.model.MessageResult; import org.thoughtcrime.securesms.conversationlist.model.SearchResult; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -106,6 +103,7 @@ import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; import org.thoughtcrime.securesms.lock.RegistrationLockDialog; +import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.megaphone.Megaphone; @@ -231,7 +229,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat initializeSearchListener(); RatingManager.showRatingDialogIfNecessary(requireContext()); - RegistrationLockDialog.showReminderIfNecessary(requireContext()); + + RegistrationLockDialog.showReminderIfNecessary(this); TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages)); } @@ -308,6 +307,14 @@ public boolean onBackPressed() { return false; } + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) { + Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show(); + viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL); + } + } + @Override public void onConversationClicked(@NonNull ThreadRecord threadRecord) { getNavigator().goToConversation(threadRecord.getRecipient().getId(), @@ -350,8 +357,13 @@ public void onMegaphoneNavigationRequested(@NonNull Intent intent) { } @Override - public void onMegaphoneToastRequested(int stringRes) { - Toast.makeText(requireContext(), stringRes, Toast.LENGTH_SHORT).show(); + public void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode) { + startActivityForResult(intent, requestCode); + } + + @Override + public void onMegaphoneToastRequested(@NonNull String string) { + Snackbar.make(fab, string, Snackbar.LENGTH_SHORT).show(); } @Override @@ -472,7 +484,7 @@ private void onMegaphoneChanged(@Nullable Megaphone megaphone) { megaphoneContainer.setVisibility(View.GONE); if (megaphone.getOnVisibleListener() != null) { - megaphone.getOnVisibleListener().onVisible(megaphone, this); + megaphone.getOnVisibleListener().onEvent(megaphone, this); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java index 864688483c3..8b2743c5f99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java @@ -1,16 +1,16 @@ package org.thoughtcrime.securesms.conversationlist; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; - import android.app.Application; import android.database.ContentObserver; import android.os.Handler; -import androidx.annotation.NonNull; import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + import org.thoughtcrime.securesms.conversationlist.model.SearchResult; import org.thoughtcrime.securesms.database.DatabaseContentProviders; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -70,7 +70,7 @@ void onMegaphoneCompleted(@NonNull Megaphones.Event event) { } void onMegaphoneSnoozed(@NonNull Megaphone snoozed) { - megaphoneRepository.markSeen(snoozed); + megaphoneRepository.markSeen(snoozed.getEvent()); megaphone.postValue(null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index df7bbd81a6d..71a6ba3ee02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.keyvalue; +import androidx.annotation.CheckResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType; import org.thoughtcrime.securesms.util.JsonUtils; import org.whispersystems.signalservice.api.RegistrationLockData; import org.whispersystems.signalservice.api.kbs.MasterKey; @@ -17,6 +19,7 @@ public final class KbsValues { private static final String MASTER_KEY = "kbs.registration_lock_master_key"; private static final String TOKEN_RESPONSE = "kbs.token_response"; private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash"; + private static final String KEYBOARD_TYPE = "kbs.keyboard_type"; private final KeyValueStore store; @@ -32,6 +35,7 @@ public void clearRegistrationLock() { .remove(V2_LOCK_ENABLED) .remove(TOKEN_RESPONSE) .remove(LOCK_LOCAL_PIN_HASH) + .remove(KEYBOARD_TYPE) .commit(); } @@ -112,4 +116,15 @@ public boolean isV2RegistrationLockEnabled() { throw new AssertionError(e); } } + + public void setKeyboardType(@NonNull KbsKeyboardType keyboardType) { + store.beginWrite() + .putString(KEYBOARD_TYPE, keyboardType.getCode()) + .commit(); + } + + @CheckResult + public @NonNull KbsKeyboardType getKeyboardType() { + return KbsKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null)); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueDataSet.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueDataSet.java index e79b154fa3e..0f61048c7bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueDataSet.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueDataSet.java @@ -106,6 +106,10 @@ public String getString(@NonNull String key, String defaultValue) { } } + boolean containsKey(@NonNull String key) { + return values.containsKey(key); + } + public @NonNull Map getValues() { return values; } @@ -114,10 +118,6 @@ public Class getType(@NonNull String key) { return types.get(key); } - public boolean containsKey(@NonNull String key) { - return values.containsKey(key); - } - private E readValueAsType(@NonNull String key, Class type, boolean nullable) { Object value = values.get(key); if ((value == null && nullable) || (value != null && value.getClass() == type)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java new file mode 100644 index 00000000000..f56c23c6ba4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/RegistrationValues.java @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.keyvalue; + +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; + +public final class RegistrationValues { + + private static final String REGISTRATION_COMPLETE = "registration.complete"; + private static final String PIN_REQUIRED = "registration.pin_required"; + + private final KeyValueStore store; + + RegistrationValues(@NonNull KeyValueStore store) { + this.store = store; + } + + public synchronized void onNewInstall() { + store.beginWrite() + .putBoolean(REGISTRATION_COMPLETE, false) + .putBoolean(PIN_REQUIRED, true) + .commit(); + } + + public synchronized void clearRegistrationComplete() { + onNewInstall(); + } + + public synchronized void setRegistrationComplete() { + store.beginWrite() + .putBoolean(REGISTRATION_COMPLETE, true) + .commit(); + } + + @CheckResult + public synchronized boolean isPinRequired() { + return store.getBoolean(PIN_REQUIRED, false); + } + + @CheckResult + public synchronized boolean isRegistrationComplete() { + return store.getBoolean(REGISTRATION_COMPLETE, true); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index 551e944fc2b..a052e4e19b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -21,6 +21,10 @@ public static KbsValues kbsValues() { return new KbsValues(getStore()); } + public static RegistrationValues registrationValues() { + return new RegistrationValues(getStore()); + } + public static String getRemoteConfig() { return getStore().getString(REMOTE_CONFIG, null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java index d96a33646e7..7bcce494f49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockDialog.java @@ -6,6 +6,7 @@ import android.os.AsyncTask; import android.os.Build; import android.text.Editable; +import android.text.InputType; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -26,13 +27,20 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.app.DialogCompat; +import androidx.fragment.app.Fragment; + +import com.google.android.material.textfield.TextInputLayout; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.KbsValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; +import org.thoughtcrime.securesms.lock.v2.KbsConstants; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; import org.thoughtcrime.securesms.util.FeatureFlags; @@ -43,7 +51,6 @@ import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.RegistrationLockData; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; @@ -55,10 +62,9 @@ public final class RegistrationLockDialog { private static final String TAG = Log.tag(RegistrationLockDialog.class); - private static final int MIN_V2_NUMERIC_PIN_LENGTH_ENTRY = 4; - private static final int MIN_V2_NUMERIC_PIN_LENGTH_SETTING = 4; + public static void showReminderIfNecessary(@NonNull Fragment fragment) { + final Context context = fragment.requireContext(); - public static void showReminderIfNecessary(@NonNull Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return; if (!RegistrationLockReminders.needsReminder(context)) return; @@ -69,6 +75,86 @@ public static void showReminderIfNecessary(@NonNull Context context) { return; } + if (FeatureFlags.pinsForAll()) { + showReminder(context, fragment); + } else { + showLegacyPinReminder(context); + } + } + + private static void showReminder(@NonNull Context context, @NonNull Fragment fragment) { + AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent) + .setView(R.layout.kbs_pin_reminder_view) + .setCancelable(false) + .setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false)) + .create(); + + WindowManager windowManager = ServiceUtil.getWindowManager(context); + Display display = windowManager.getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + display.getMetrics(metrics); + + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + dialog.show(); + dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT); + + TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper); + EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin); + TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder); + View skip = DialogCompat.requireViewById(dialog, R.id.skip); + View submit = DialogCompat.requireViewById(dialog, R.id.submit); + + SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin)); + SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin)); + + pinEditText.requestFocus(); + + switch (SignalStore.kbsValues().getKeyboardType()) { + case NUMERIC: + pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + break; + case ALPHA_NUMERIC: + pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + break; + } + + ClickableSpan clickableSpan = new ClickableSpan() { + @Override + public void onClick(@NonNull View widget) { + dialog.dismiss(); + RegistrationLockReminders.scheduleReminder(context, true); + + fragment.startActivityForResult(CreateKbsPinActivity.getIntentForPinUpdate(context), CreateKbsPinActivity.REQUEST_NEW_PIN); + } + }; + + forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText)); + reminder.setMovementMethod(LinkMovementMethod.getInstance()); + + skip.setOnClickListener(v -> { + dialog.dismiss(); + RegistrationLockReminders.scheduleReminder(context, false); + }); + + PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper); + PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled() + ? new V2PinVerifier() + : new V1PinVerifier(context); + + submit.setOnClickListener(v -> { + Editable pinEditable = pinEditText.getText(); + + verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback); + }); + } + + /** + * @deprecated TODO [alex]: Remove after pins for all live. + */ + @Deprecated + private static void showLegacyPinReminder(@NonNull Context context) { AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight) .setView(R.layout.registration_lock_reminder_view) .setCancelable(true) @@ -84,8 +170,8 @@ public static void showReminderIfNecessary(@NonNull Context context) { dialog.show(); dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT); - EditText pinEditText = dialog.findViewById(R.id.pin); - TextView reminder = dialog.findViewById(R.id.reminder); + EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin); + TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder); if (pinEditText == null) throw new AssertionError(); if (reminder == null) throw new AssertionError(); @@ -136,17 +222,15 @@ private static TextWatcher getV1PinWatcher(@NonNull Context context, AlertDialog private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) { KbsValues kbsValues = SignalStore.kbsValues(); - MasterKey masterKey = kbsValues.getPinBackedMasterKey(); String localPinHash = kbsValues.getLocalPinHash(); - if (masterKey == null) throw new AssertionError("No masterKey set at time of reminder"); if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder"); return new AfterTextChanged((Editable s) -> { if (s == null) return; String pin = s.toString(); if (TextUtils.isEmpty(pin)) return; - if (pin.length() < MIN_V2_NUMERIC_PIN_LENGTH_ENTRY) return; + if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return; if (PinHashing.verifyLocalPinHash(localPinHash, pin)) { dialog.dismiss(); @@ -178,9 +262,9 @@ public static void showRegistrationLockPrompt(@NonNull Context context, @NonNull String pinValue = pin.getText().toString().replace(" ", ""); String repeatValue = repeat.getText().toString().replace(" ", ""); - if (pinValue.length() < MIN_V2_NUMERIC_PIN_LENGTH_SETTING) { + if (pinValue.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) { Toast.makeText(context, - context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, MIN_V2_NUMERIC_PIN_LENGTH_SETTING), + context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH), Toast.LENGTH_LONG).show(); return; } @@ -325,4 +409,78 @@ protected void onPostExecute(Boolean result) { dialog.show(); } + private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context, + @NonNull AlertDialog dialog, + @NonNull TextInputLayout inputWrapper) + { + return new PinVerifier.Callback() { + @Override + public void onPinCorrect() { + dialog.dismiss(); + RegistrationLockReminders.scheduleReminder(context, true); + } + + @Override + public void onPinWrong() { + inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again)); + } + }; + } + + private static final class V1PinVerifier implements PinVerifier { + + private final String pinInPreferences; + + private V1PinVerifier(@NonNull Context context) { + //noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system. + this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context); + } + + @Override + public void verifyPin(@Nullable String pin, @NonNull Callback callback) { + if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) { + callback.onPinCorrect(); + + Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2"); + ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob()); + } else { + callback.onPinWrong(); + } + } + } + + private static final class V2PinVerifier implements PinVerifier { + + private final String localPinHash; + + V2PinVerifier() { + localPinHash = SignalStore.kbsValues().getLocalPinHash(); + + if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder"); + } + + @Override + public void verifyPin(@Nullable String pin, @NonNull Callback callback) { + if (pin == null) return; + if (TextUtils.isEmpty(pin)) return; + + if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return; + + if (PinHashing.verifyLocalPinHash(localPinHash, pin)) { + callback.onPinCorrect(); + } else { + callback.onPinWrong(); + } + } + } + + private interface PinVerifier { + + void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback); + + interface Callback { + void onPinCorrect(); + void onPinWrong(); + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java index 9a2d1637d49..bb34bad54c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/RegistrationLockReminders.java @@ -35,19 +35,19 @@ public static boolean needsReminder(@NonNull Context context) { } public static void scheduleReminder(@NonNull Context context, boolean success) { - Long nextReminderInterval; - if (success) { long timeSinceLastReminder = System.currentTimeMillis() - TextSecurePreferences.getRegistrationLockLastReminderTime(context); - nextReminderInterval = INTERVALS.higher(timeSinceLastReminder); - if (nextReminderInterval == null) nextReminderInterval = INTERVALS.last(); + Long nextReminderInterval = INTERVALS.higher(timeSinceLastReminder); + + if (nextReminderInterval == null) { + nextReminderInterval = INTERVALS.last(); + } + + TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); + TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval); } else { - long lastReminderInterval = TextSecurePreferences.getRegistrationLockNextReminderInterval(context); - nextReminderInterval = INTERVALS.lower(lastReminderInterval); - if (nextReminderInterval == null) nextReminderInterval = INTERVALS.first(); + long timeSinceLastReminder = TextSecurePreferences.getRegistrationLockLastReminderTime(context) + TimeUnit.MINUTES.toMillis(5); + TextSecurePreferences.setRegistrationLockLastReminderTime(context, timeSinceLastReminder); } - - TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); - TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java new file mode 100644 index 00000000000..dca0c32aa23 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java @@ -0,0 +1,149 @@ +package org.thoughtcrime.securesms.lock.v2; + +import android.os.Bundle; +import android.text.InputType; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.fragment.app.Fragment; + +import com.airbnb.lottie.LottieAnimationView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.text.AfterTextChanged; + +abstract class BaseKbsPinFragment extends Fragment { + + private TextView title; + private TextView description; + private EditText input; + private TextView label; + private TextView keyboardToggle; + private TextView confirm; + private LottieAnimationView lottieProgress; + private LottieAnimationView lottieEnd; + private ViewModel viewModel; + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) + { + return inflater.inflate(R.layout.base_kbs_pin_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + initializeViews(view); + + viewModel = initializeViewModel(); + viewModel.getUserEntry().observe(getViewLifecycleOwner(), kbsPin -> { + boolean isEntryValid = kbsPin.length() >= KbsConstants.MINIMUM_NEW_PIN_LENGTH; + + confirm.setEnabled(isEntryValid); + confirm.setAlpha(isEntryValid ? 1f : 0.5f); + }); + + viewModel.getKeyboard().observe(getViewLifecycleOwner(), keyboardType -> { + updateKeyboard(keyboardType); + keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); + }); + + initializeListeners(); + } + + @Override + public void onResume() { + super.onResume(); + + input.requestFocus(); + } + + protected abstract ViewModel initializeViewModel(); + + protected abstract void initializeViewStates(); + + protected TextView getTitle() { + return title; + } + + protected TextView getDescription() { + return description; + } + + protected EditText getInput() { + return input; + } + + protected LottieAnimationView getLottieProgress() { + return lottieProgress; + } + + protected LottieAnimationView getLottieEnd() { + return lottieEnd; + } + + protected TextView getLabel() { + return label; + } + + protected TextView getKeyboardToggle() { + return keyboardToggle; + } + + protected TextView getConfirm() { + return confirm; + } + + private void initializeViews(@NonNull View view) { + title = view.findViewById(R.id.edit_kbs_pin_title); + description = view.findViewById(R.id.edit_kbs_pin_description); + input = view.findViewById(R.id.edit_kbs_pin_input); + label = view.findViewById(R.id.edit_kbs_pin_input_label); + keyboardToggle = view.findViewById(R.id.edit_kbs_pin_keyboard_toggle); + confirm = view.findViewById(R.id.edit_kbs_pin_confirm); + lottieProgress = view.findViewById(R.id.edit_kbs_pin_lottie_progress); + lottieEnd = view.findViewById(R.id.edit_kbs_pin_lottie_end); + + initializeViewStates(); + } + + private void initializeListeners() { + input.addTextChangedListener(new AfterTextChanged(s -> viewModel.setUserEntry(s.toString()))); + input.setImeOptions(EditorInfo.IME_ACTION_NEXT); + input.setOnEditorActionListener(this::handleEditorAction); + keyboardToggle.setOnClickListener(v -> viewModel.toggleAlphaNumeric()); + confirm.setOnClickListener(v -> viewModel.confirm()); + } + + private boolean handleEditorAction(@NonNull View view, int actionId, @NonNull KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_NEXT && confirm.isEnabled()) { + viewModel.confirm(); + } + + return true; + } + + private void updateKeyboard(@NonNull KbsKeyboardType keyboard) { + boolean isAlphaNumeric = keyboard == KbsKeyboardType.ALPHA_NUMERIC; + + input.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD + : InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + } + + private @StringRes int resolveKeyboardToggleText(@NonNull KbsKeyboardType keyboard) { + if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) { + return R.string.BaseKbsPinFragment__create_numeric_pin; + } else { + return R.string.BaseKbsPinFragment__create_alphanumeric_pin; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinViewModel.java new file mode 100644 index 00000000000..e991d69b3cd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinViewModel.java @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.lock.v2; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +interface BaseKbsPinViewModel { + LiveData getUserEntry(); + + LiveData getKeyboard(); + + @MainThread + void setUserEntry(String userEntry); + + @MainThread + void toggleAlphaNumeric(); + + @MainThread + void confirm(); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java new file mode 100644 index 00000000000..af85c7dcdae --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java @@ -0,0 +1,186 @@ +package org.thoughtcrime.securesms.lock.v2; + +import android.animation.Animator; +import android.app.Activity; +import android.content.Intent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.RawRes; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.util.Preconditions; +import androidx.lifecycle.ViewModelProviders; + +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieDrawable; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.animation.AnimationCompleteListener; +import org.thoughtcrime.securesms.animation.AnimationRepeatListener; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.megaphone.Megaphones; +import org.thoughtcrime.securesms.util.SpanUtil; + +public class ConfirmKbsPinFragment extends BaseKbsPinFragment { + + private ConfirmKbsPinFragmentArgs args; + private ConfirmKbsPinViewModel viewModel; + + @Override + protected void initializeViewStates() { + args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments()); + + if (args.getIsNewPin()) { + initializeViewStatesForNewPin(); + } else { + initializeViewStatesForPin(); + } + } + + @Override + protected ConfirmKbsPinViewModel initializeViewModel() { + KbsPin userEntry = Preconditions.checkNotNull(args.getUserEntry()); + KbsKeyboardType keyboard = args.getKeyboard(); + ConfirmKbsPinRepository repository = new ConfirmKbsPinRepository(); + ConfirmKbsPinViewModel.Factory factory = new ConfirmKbsPinViewModel.Factory(userEntry, keyboard, repository); + + viewModel = ViewModelProviders.of(this, factory).get(ConfirmKbsPinViewModel.class); + + viewModel.getLabel().observe(getViewLifecycleOwner(), this::updateLabel); + viewModel.getSaveAnimation().observe(getViewLifecycleOwner(), this::updateSaveAnimation); + + return viewModel; + } + + private void initializeViewStatesForNewPin() { + getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin); + getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin); + getKeyboardToggle().setVisibility(View.INVISIBLE); + getLabel().setText(""); + } + + private void initializeViewStatesForPin() { + getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin); + getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin); + getKeyboardToggle().setVisibility(View.INVISIBLE); + getLabel().setText(""); + } + + private void updateLabel(@NonNull ConfirmKbsPinViewModel.Label label) { + switch (label) { + case EMPTY: + getLabel().setText(""); + break; + case CREATING_PIN: + getLabel().setText(R.string.ConfirmKbsPinFragment__creating_pin); + break; + case RE_ENTER_PIN: + getLabel().setText(R.string.ConfirmKbsPinFragment__re_enter_pin); + break; + case PIN_DOES_NOT_MATCH: + getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red), + getString(R.string.ConfirmKbsPinFragment__pins_dont_match))); + break; + } + } + + private void updateSaveAnimation(@NonNull ConfirmKbsPinViewModel.SaveAnimation animation) { + updateAnimationAndInputVisibility(animation); + LottieAnimationView lottieProgress = getLottieProgress(); + + switch (animation) { + case NONE: + lottieProgress.cancelAnimation(); + break; + case LOADING: + lottieProgress.setAnimation(R.raw.lottie_kbs_loading); + lottieProgress.setRepeatMode(LottieDrawable.RESTART); + lottieProgress.setRepeatCount(LottieDrawable.INFINITE); + lottieProgress.playAnimation(); + break; + case SUCCESS: + startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_success, new AnimationCompleteListener() { + @Override + public void onAnimationEnd(Animator animation) { + requireActivity().setResult(Activity.RESULT_OK); + closeNavGraphBranch(); + } + }); + break; + case FAILURE: + startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() { + @Override + public void onAnimationEnd(Animator animation) { + displayFailedDialog(); + } + }); + break; + } + } + + private void startEndAnimationOnNextProgressRepetition(@RawRes int lottieAnimationId, + @NonNull AnimationCompleteListener listener) + { + LottieAnimationView lottieProgress = getLottieProgress(); + LottieAnimationView lottieEnd = getLottieEnd(); + + lottieEnd.setAnimation(lottieAnimationId); + lottieEnd.removeAllAnimatorListeners(); + lottieEnd.setRepeatCount(0); + lottieEnd.addAnimatorListener(listener); + + if (lottieProgress.isAnimating()) { + lottieProgress.addAnimatorListener(new AnimationRepeatListener(animator -> + hideProgressAndStartEndAnimation(lottieProgress, lottieEnd) + )); + } else { + hideProgressAndStartEndAnimation(lottieProgress, lottieEnd); + } + } + + private void hideProgressAndStartEndAnimation(@NonNull LottieAnimationView lottieProgress, + @NonNull LottieAnimationView lottieEnd) + { + viewModel.onLoadingAnimationComplete(); + lottieProgress.setVisibility(View.GONE); + lottieEnd.setVisibility(View.VISIBLE); + lottieEnd.playAnimation(); + } + + private void updateAnimationAndInputVisibility(ConfirmKbsPinViewModel.SaveAnimation saveAnimation) { + if (saveAnimation == ConfirmKbsPinViewModel.SaveAnimation.NONE) { + getInput().setVisibility(View.VISIBLE); + getLottieProgress().setVisibility(View.GONE); + } else { + getInput().setVisibility(View.GONE); + getLottieProgress().setVisibility(View.VISIBLE); + } + } + + private void displayFailedDialog() { + new AlertDialog.Builder(requireContext()).setTitle(R.string.ConfirmKbsPinFragment__pin_creation_failed) + .setMessage(R.string.ConfirmKbsPinFragment__your_pin_was_not_saved) + .setCancelable(false) + .setPositiveButton(R.string.ok, (d, w) -> { + d.dismiss(); + markMegaphoneSeenIfNecessary(); + requireActivity().setResult(Activity.RESULT_CANCELED); + closeNavGraphBranch(); + }) + .show(); + } + + private void closeNavGraphBranch() { + Intent activityIntent = requireActivity().getIntent(); + if (activityIntent != null && activityIntent.hasExtra("next_intent")) { + startActivity(activityIntent.getParcelableExtra("next_intent")); + } + + requireActivity().finish(); + } + + private void markMegaphoneSeenIfNecessary() { + ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.PINS_FOR_ALL); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinRepository.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinRepository.java new file mode 100644 index 00000000000..3c1324cd1ed --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinRepository.java @@ -0,0 +1,75 @@ +package org.thoughtcrime.securesms.lock.v2; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.KbsValues; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.PinHashing; +import org.thoughtcrime.securesms.lock.RegistrationLockReminders; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.megaphone.Megaphones; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import org.whispersystems.signalservice.api.KeyBackupService; +import org.whispersystems.signalservice.api.KeyBackupServicePinException; +import org.whispersystems.signalservice.api.RegistrationLockData; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.kbs.HashedPin; +import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; + +import java.io.IOException; + +final class ConfirmKbsPinRepository { + + private static final String TAG = Log.tag(ConfirmKbsPinRepository.class); + + void setPin(@NonNull KbsPin kbsPin, @NonNull KbsKeyboardType keyboard, @NonNull Consumer resultConsumer) { + + Context context = ApplicationDependencies.getApplication(); + String pinValue = kbsPin.toString(); + + SimpleTask.run(() -> { + try { + Log.i(TAG, "Setting pin on KBS"); + + KbsValues kbsValues = SignalStore.kbsValues(); + MasterKey masterKey = kbsValues.getOrCreateMasterKey(); + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession); + RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey); + RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse()) + .restorePin(hashedPin); + + if (!restoredData.getMasterKey().equals(masterKey)) { + throw new AssertionError("Failed to set the pin correctly"); + } else { + Log.i(TAG, "Set and retrieved pin on KBS successfully"); + } + + kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue)); + TextSecurePreferences.clearOldRegistrationLockPin(context); + TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); + TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); + SignalStore.kbsValues().setKeyboardType(keyboard); + ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL); + + return PinSetResult.SUCCESS; + } catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException e) { + Log.w(TAG, e); + return PinSetResult.FAILURE; + } + }, resultConsumer::accept); + } + + enum PinSetResult { + SUCCESS, + FAILURE + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinViewModel.java new file mode 100644 index 00000000000..855e0085315 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinViewModel.java @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.lock.v2; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.core.util.Preconditions; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Transformations; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinRepository.PinSetResult; + +final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel { + + private final ConfirmKbsPinRepository repository; + + private final MutableLiveData userEntry = new MutableLiveData<>(KbsPin.EMPTY); + private final MutableLiveData keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC); + private final MutableLiveData saveAnimation = new MutableLiveData<>(SaveAnimation.NONE); + private final MutableLiveData