diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8e94898866..c054a91e59 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 c554cfa1ea..3da01a9ee9 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.FcmRefreshJob;
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 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
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 5036cff21c..b5c9db1a72 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 androidx.fragment.app.Fragment;
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 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
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 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
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 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
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 0000000000..28317e9a49
--- /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 8a4ead88e1..d80bbe7b27 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -33,33 +33,6 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
-
-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.widget.Toolbar;
-import androidx.appcompat.widget.TooltipCompat;
-import androidx.lifecycle.DefaultLifecycleObserver;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.ProcessLifecycleOwner;
-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.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.ItemTouchHelper;
-
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -70,6 +43,30 @@ 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 androidx.annotation.PluralsRes;
+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;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.ProcessLifecycleOwner;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.snackbar.Snackbar;
+
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -78,7 +75,6 @@ import org.thoughtcrime.securesms.MainFragment;
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.ServiceOutageReminder;
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.events.ReminderUpdateEvent;
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 class ConversationListFragment extends MainFragment implements LoaderMana
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 class ConversationListFragment extends MainFragment implements LoaderMana
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 class ConversationListFragment extends MainFragment implements LoaderMana
}
@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 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
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 864688483c..8b2743c5f9 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 @@ class ConversationListViewModel extends ViewModel {
}
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 df7bbd81a6..71a6ba3ee0 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 final class KbsValues {
.remove(V2_LOCK_ENABLED)
.remove(TOKEN_RESPONSE)
.remove(LOCK_LOCAL_PIN_HASH)
+ .remove(KEYBOARD_TYPE)
.commit();
}
@@ -112,4 +116,15 @@ public final class KbsValues {
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 e79b154fa3..0f61048c7b 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 class KeyValueDataSet implements KeyValueReader {
}
}
+ boolean containsKey(@NonNull String key) {
+ return values.containsKey(key);
+ }
+
public @NonNull Map getValues() {
return values;
}
@@ -114,10 +118,6 @@ public class KeyValueDataSet implements KeyValueReader {
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 0000000000..f56c23c6ba
--- /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 551e944fc2..a052e4e19b 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 final class SignalStore {
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 d96a33646e..7bcce494f4 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.graphics.Typeface;
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.TextView;
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.thoughtcrime.securesms.util.text.AfterTextChanged;
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 final class RegistrationLockDialog {
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 final class RegistrationLockDialog {
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 @@ public final class RegistrationLockDialog {
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 final class RegistrationLockDialog {
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 @@ public final class RegistrationLockDialog {
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 9a2d1637d4..bb34bad54c 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 class RegistrationLockReminders {
}
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();
- } else {
- long lastReminderInterval = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);
- nextReminderInterval = INTERVALS.lower(lastReminderInterval);
- if (nextReminderInterval == null) nextReminderInterval = INTERVALS.first();
- }
+ Long nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
- TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
- TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
+ if (nextReminderInterval == null) {
+ nextReminderInterval = INTERVALS.last();
+ }
+
+ TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
+ TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
+ } else {
+ long timeSinceLastReminder = TextSecurePreferences.getRegistrationLockLastReminderTime(context) + TimeUnit.MINUTES.toMillis(5);
+ TextSecurePreferences.setRegistrationLockLastReminderTime(context, timeSinceLastReminder);
+ }
}
}
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 0000000000..dca0c32aa2
--- /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 0000000000..e991d69b3c
--- /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 0000000000..af85c7dcda
--- /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 0000000000..3c1324cd1e
--- /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 0000000000..855e008531
--- /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