Implement new PIN UX.

master
Alex Hart 2020-01-30 16:23:29 -04:00
parent 109d67956f
commit fb82420376
71 changed files with 3000 additions and 203 deletions

View File

@ -414,11 +414,21 @@
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".lock.v2.KbsMigrationActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ClearProfileAvatarActivity"
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">
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">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"/>

View File

@ -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));
}

View File

@ -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);

View File

@ -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<Animator> animationConsumer;
public AnimationRepeatListener(@NonNull Consumer<Animator> 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View File

@ -106,6 +106,10 @@ public class KeyValueDataSet implements KeyValueReader {
}
}
boolean containsKey(@NonNull String key) {
return values.containsKey(key);
}
public @NonNull Map<String, Object> 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> E readValueAsType(@NonNull String key, Class<E> type, boolean nullable) {
Object value = values.get(key);
if ((value == null && nullable) || (value != null && value.getClass() == type)) {

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<ViewModel extends BaseKbsPinViewModel> 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;
}
}
}

View File

@ -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<KbsPin> getUserEntry();
LiveData<KbsKeyboardType> getKeyboard();
@MainThread
void setUserEntry(String userEntry);
@MainThread
void toggleAlphaNumeric();
@MainThread
void confirm();
}

View File

@ -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<ConfirmKbsPinViewModel> {
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);
}
}

View File

@ -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<PinSetResult> 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
}
}

View File

@ -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<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
private final MutableLiveData<KbsKeyboardType> keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC);
private final MutableLiveData<SaveAnimation> saveAnimation = new MutableLiveData<>(SaveAnimation.NONE);
private final MutableLiveData<Label> label = new MutableLiveData<>(Label.RE_ENTER_PIN);
private final KbsPin pinToConfirm;
private ConfirmKbsPinViewModel(@NonNull KbsPin pinToConfirm,
@NonNull KbsKeyboardType keyboard,
@NonNull ConfirmKbsPinRepository repository)
{
this.keyboard.setValue(keyboard);
this.pinToConfirm = pinToConfirm;
this.repository = repository;
}
LiveData<SaveAnimation> getSaveAnimation() {
return Transformations.distinctUntilChanged(saveAnimation);
}
LiveData<Label> getLabel() {
return Transformations.distinctUntilChanged(label);
}
@Override
public void confirm() {
KbsPin userEntry = this.userEntry.getValue();
if (pinToConfirm.toString().equals(userEntry.toString())) {
this.label.setValue(Label.CREATING_PIN);
this.userEntry.setValue(KbsPin.EMPTY);
this.saveAnimation.setValue(SaveAnimation.LOADING);
repository.setPin(pinToConfirm, Preconditions.checkNotNull(this.keyboard.getValue()), this::handleResult);
} else {
this.label.setValue(Label.PIN_DOES_NOT_MATCH);
}
}
void onLoadingAnimationComplete() {
this.label.setValue(Label.EMPTY);
}
@Override
public LiveData<KbsPin> getUserEntry() {
return userEntry;
}
@Override
public LiveData<KbsKeyboardType> getKeyboard() {
return keyboard;
}
@MainThread
public void setUserEntry(String userEntry) {
this.userEntry.setValue(KbsPin.from(userEntry));
}
@MainThread
public void toggleAlphaNumeric() {
this.keyboard.setValue(this.keyboard.getValue().getOther());
}
private void handleResult(PinSetResult result) {
switch (result) {
case SUCCESS:
this.saveAnimation.setValue(SaveAnimation.SUCCESS);
break;
case FAILURE:
this.saveAnimation.setValue(SaveAnimation.FAILURE);
break;
default:
throw new IllegalStateException("Unknown state: " + result.name());
}
}
enum Label {
RE_ENTER_PIN,
PIN_DOES_NOT_MATCH,
CREATING_PIN,
EMPTY
}
enum SaveAnimation {
NONE,
LOADING,
SUCCESS,
FAILURE
}
static final class Factory implements ViewModelProvider.Factory {
private final KbsPin pinToConfirm;
private final KbsKeyboardType keyboard;
private final ConfirmKbsPinRepository repository;
Factory(@NonNull KbsPin pinToConfirm,
@NonNull KbsKeyboardType keyboard,
@NonNull ConfirmKbsPinRepository repository)
{
this.pinToConfirm = pinToConfirm;
this.keyboard = keyboard;
this.repository = repository;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new ConfirmKbsPinViewModel(pinToConfirm, keyboard, repository);
}
}
}

View File

@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.PassphrasePromptActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class CreateKbsPinActivity extends BaseActionBarActivity {
public static final int REQUEST_NEW_PIN = 27698;
private static final String IS_NEW_PIN = "is_new_pin";
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
public static Intent getIntentForPinCreate(@NonNull Context context) {
return new Intent(context, CreateKbsPinActivity.class);
}
public static Intent getIntentForPinUpdate(@NonNull Context context) {
Intent intent = getIntentForPinCreate(context);
intent.putExtra(IS_NEW_PIN, true);
return intent;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
if (KeyCachingService.isLocked(this)) {
startActivity(getPromptPassphraseIntent());
finish();
return;
}
dynamicTheme.onCreate(this);
setContentView(R.layout.create_kbs_pin_activity);
CreateKbsPinFragmentArgs arguments = CreateKbsPinFragmentArgs.fromBundle(getIntent().getExtras());
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle());
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private Intent getPromptPassphraseIntent() {
return getRoutedIntent(PassphrasePromptActivity.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);
return intent;
}
}

View File

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
private CreateKbsPinFragmentArgs args;
@Override
protected void initializeViewStates() {
args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
if (args.getIsNewPin()) {
initializeViewStatesForNewPin();
} else {
initializeViewStatesForPin();
}
getLabel().setText(getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits));
getConfirm().setEnabled(false);
}
private void initializeViewStatesForPin() {
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
getDescription().setText(R.string.CreateKbsPinFragment__pins_add_an_extra_layer_of_security);
}
private void initializeViewStatesForNewPin() {
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
getDescription().setText(R.string.CreateKbsPinFragment__because_youre_still_logged_in);
}
@Override
protected CreateKbsPinViewModel initializeViewModel() {
CreateKbsPinViewModel viewModel = ViewModelProviders.of(this).get(CreateKbsPinViewModel.class);
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> getLabel().setText(getLabelText(k)));
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard()));
return viewModel;
}
private void onConfirmPin(@NonNull KbsPin userEntry, @NonNull KbsKeyboardType keyboard) {
CreateKbsPinFragmentDirections.ActionConfirmPin action = CreateKbsPinFragmentDirections.actionConfirmPin();
action.setUserEntry(userEntry);
action.setKeyboard(keyboard);
action.setIsNewPin(args.getIsNewPin());
Navigation.findNavController(requireView()).navigate(action);
}
private String getLabelText(@NonNull KbsKeyboardType keyboard) {
if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) {
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_characters);
} else {
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits);
}
}
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_NEW_PIN_LENGTH, KbsConstants.MINIMUM_NEW_PIN_LENGTH);
}
}

View File

@ -0,0 +1,69 @@
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.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
private final MutableLiveData<KbsKeyboardType> keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC);
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
@Override
public LiveData<KbsPin> getUserEntry() {
return userEntry;
}
@Override
public LiveData<KbsKeyboardType> getKeyboard() {
return keyboard;
}
LiveData<NavigationEvent> getNavigationEvents() { return events; }
@Override
@MainThread
public void setUserEntry(String userEntry) {
this.userEntry.setValue(KbsPin.from(userEntry));
}
@Override
@MainThread
public void toggleAlphaNumeric() {
this.keyboard.setValue(Preconditions.checkNotNull(this.keyboard.getValue()).getOther());
}
@Override
@MainThread
public void confirm() {
events.setValue(new NavigationEvent(Preconditions.checkNotNull(this.getUserEntry().getValue()),
Preconditions.checkNotNull(this.getKeyboard().getValue())));
}
static final class NavigationEvent {
private final KbsPin userEntry;
private final KbsKeyboardType keyboard;
NavigationEvent(@NonNull KbsPin userEntry, @NonNull KbsKeyboardType keyboard) {
this.userEntry = userEntry;
this.keyboard = keyboard;
}
KbsPin getUserEntry() {
return userEntry;
}
KbsKeyboardType getKeyboard() {
return keyboard;
}
}
}

View File

@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.lock.v2;
public final class KbsConstants {
static final int MINIMUM_NEW_PIN_LENGTH = 6;
/** Migrated pins from V1 might be 4 */
public static final int MINIMUM_POSSIBLE_PIN_LENGTH = 4;
private KbsConstants() {
}
}

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.lock.v2;
import androidx.annotation.Nullable;
public enum KbsKeyboardType {
NUMERIC("numeric"),
ALPHA_NUMERIC("alphaNumeric");
private final String code;
KbsKeyboardType(String code) {
this.code = code;
}
KbsKeyboardType getOther() {
if (this == NUMERIC) return ALPHA_NUMERIC;
else return NUMERIC;
}
public String getCode() {
return code;
}
public static KbsKeyboardType fromCode(@Nullable String code) {
for (KbsKeyboardType type : KbsKeyboardType.values()) {
if (type.code.equals(code)) {
return type;
}
}
return NUMERIC;
}
}

View File

@ -0,0 +1,56 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.PassphrasePromptActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class KbsMigrationActivity extends BaseActionBarActivity {
public static final int REQUEST_NEW_PIN = CreateKbsPinActivity.REQUEST_NEW_PIN;
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
public static Intent createIntent() {
return new Intent(ApplicationDependencies.getApplication(), KbsMigrationActivity.class);
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
if (KeyCachingService.isLocked(this)) {
startActivity(getPromptPassphraseIntent());
finish();
return;
}
dynamicTheme.onCreate(this);
setContentView(R.layout.kbs_migration_activity);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private Intent getPromptPassphraseIntent() {
return getRoutedIntent(PassphrasePromptActivity.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);
return intent;
}
}

View File

@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.lock.v2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public final class KbsPin implements Parcelable {
public static KbsPin EMPTY = new KbsPin("");
private final String pin;
private KbsPin(String pin) {
this.pin = pin;
}
private KbsPin(Parcel in) {
pin = in.readString();
}
@Override
public @NonNull String toString() {
return pin;
}
public static KbsPin from(@Nullable String pin) {
if (pin == null) return EMPTY;
pin = pin.trim();
if (pin.length() == 0) return EMPTY;
return new KbsPin(pin);
}
public int length() {
return pin.length();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(pin);
}
public static final Creator<KbsPin> CREATOR = new Creator<KbsPin>() {
@Override
public KbsPin createFromParcel(Parcel in) {
return new KbsPin(in);
}
@Override
public KbsPin[] newArray(int size) {
return new KbsPin[size];
}
};
}

View File

@ -0,0 +1,87 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.navigation.Navigation;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public final class KbsSplashFragment extends Fragment {
private TextView title;
private TextView description;
private TextView primaryAction;
private TextView secondaryAction;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.kbs_splash_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
title = view.findViewById(R.id.kbs_splash_title);
description = view.findViewById(R.id.kbs_splash_description);
primaryAction = view.findViewById(R.id.kbs_splash_primary_action);
secondaryAction = view.findViewById(R.id.kbs_splash_secondary_action);
primaryAction.setOnClickListener(v -> onCreatePin());
secondaryAction.setOnClickListener(v -> onLearnMore());
if (TextSecurePreferences.isV1RegistrationLockEnabled(requireContext())) {
setUpRegLockEnabled();
} else {
setUpRegLockDisabled();
}
description.setMovementMethod(LinkMovementMethod.getInstance());
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() { }
});
}
private void setUpRegLockEnabled() {
title.setText(R.string.KbsSplashFragment__registration_lock_equals_pin);
description.setText(R.string.KbsSplashFragment__your_registration_lock_is_now_called_a_pin);
primaryAction.setText(R.string.KbsSplashFragment__update_pin);
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
}
private void setUpRegLockDisabled() {
title.setText(R.string.KbsSplashFragment__introducing_pins);
description.setText(R.string.KbsSplashFragment__pins_add_another_level_of_security_to_your_account);
primaryAction.setText(R.string.KbsSplashFragment__create_your_pin);
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
}
private void onCreatePin() {
Navigation.findNavController(requireView()).navigate(KbsSplashFragmentDirections.actionCreateKbsPin());
}
private void onLearnMore() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.KbsSplashFragment__learn_more_link)));
startActivity(intent);
}
}

View File

@ -0,0 +1,18 @@
package org.thoughtcrime.securesms.lock.v2;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public final class PinUtil {
private PinUtil() {}
public static boolean userHasPin(@NonNull Context context) {
return TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
}
}

View File

@ -48,7 +48,7 @@ public class BasicMegaphoneView extends FrameLayout {
super.onAttachedToWindow();
if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
megaphone.getOnVisibleListener().onVisible(megaphone, megaphoneListener);
megaphone.getOnVisibleListener().onEvent(megaphone, megaphoneListener);
}
}
@ -82,7 +82,7 @@ public class BasicMegaphoneView extends FrameLayout {
actionButton.setText(megaphone.getButtonText());
actionButton.setOnClickListener(v -> {
if (megaphone.getButtonClickListener() != null) {
megaphone.getButtonClickListener().onClick(megaphone, megaphoneListener);
megaphone.getButtonClickListener().onEvent(megaphone, megaphoneListener);
}
});
} else {
@ -91,7 +91,13 @@ public class BasicMegaphoneView extends FrameLayout {
if (megaphone.canSnooze()) {
snoozeButton.setVisibility(VISIBLE);
snoozeButton.setOnClickListener(v -> megaphoneListener.onMegaphoneSnooze(megaphone));
snoozeButton.setOnClickListener(v -> {
megaphoneListener.onMegaphoneSnooze(megaphone);
if (megaphone.getSnoozeListener() != null) {
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
}
});
} else {
actionButton.setVisibility(GONE);
}

View File

@ -12,32 +12,29 @@ import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
*/
public class Megaphone {
/** For {@link #getMaxAppearances()}. */
public static final int UNLIMITED = -1;
private final Event event;
private final Style style;
private final boolean mandatory;
private final boolean canSnooze;
private final int maxAppearances;
private final int titleRes;
private final int bodyRes;
private final int imageRes;
private final int buttonTextRes;
private final OnClickListener buttonListener;
private final OnVisibleListener onVisibleListener;
private final Event event;
private final Style style;
private final boolean mandatory;
private final boolean canSnooze;
private final int titleRes;
private final int bodyRes;
private final int imageRes;
private final int buttonTextRes;
private final EventListener buttonListener;
private final EventListener snoozeListener;
private final EventListener onVisibleListener;
private Megaphone(@NonNull Builder builder) {
this.event = builder.event;
this.style = builder.style;
this.mandatory = builder.mandatory;
this.canSnooze = builder.canSnooze;
this.maxAppearances = builder.maxAppearances;
this.titleRes = builder.titleRes;
this.bodyRes = builder.bodyRes;
this.imageRes = builder.imageRes;
this.buttonTextRes = builder.buttonTextRes;
this.buttonListener = builder.buttonListener;
this.snoozeListener = builder.snoozeListener;
this.onVisibleListener = builder.onVisibleListener;
}
@ -49,10 +46,6 @@ public class Megaphone {
return mandatory;
}
public int getMaxAppearances() {
return maxAppearances;
}
public boolean canSnooze() {
return canSnooze;
}
@ -77,11 +70,15 @@ public class Megaphone {
return buttonTextRes;
}
public @Nullable OnClickListener getButtonClickListener() {
public @Nullable EventListener getButtonClickListener() {
return buttonListener;
}
public @Nullable OnVisibleListener getOnVisibleListener() {
public @Nullable EventListener getSnoozeListener() {
return buttonListener;
}
public @Nullable EventListener getOnVisibleListener() {
return onVisibleListener;
}
@ -90,21 +87,20 @@ public class Megaphone {
private final Event event;
private final Style style;
private boolean mandatory;
private boolean canSnooze;
private int maxAppearances;
private int titleRes;
private int bodyRes;
private int imageRes;
private int buttonTextRes;
private OnClickListener buttonListener;
private OnVisibleListener onVisibleListener;
private boolean mandatory;
private boolean canSnooze;
private int titleRes;
private int bodyRes;
private int imageRes;
private int buttonTextRes;
private EventListener buttonListener;
private EventListener snoozeListener;
private EventListener onVisibleListener;
public Builder(@NonNull Event event, @NonNull Style style) {
this.event = event;
this.style = style;
this.maxAppearances = 1;
}
public @NonNull Builder setMandatory(boolean mandatory) {
@ -112,13 +108,15 @@ public class Megaphone {
return this;
}
public @NonNull Builder setSnooze(boolean canSnooze) {
this.canSnooze = canSnooze;
public @NonNull Builder enableSnooze(@Nullable EventListener listener) {
this.canSnooze = true;
this.snoozeListener = listener;
return this;
}
public @NonNull Builder setMaxAppearances(int maxAppearances) {
this.maxAppearances = maxAppearances;
public @NonNull Builder disableSnooze() {
this.canSnooze = false;
this.snoozeListener = null;
return this;
}
@ -137,13 +135,13 @@ public class Megaphone {
return this;
}
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull OnClickListener listener) {
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull EventListener listener) {
this.buttonTextRes = buttonTextRes;
this.buttonListener = listener;
return this;
}
public @NonNull Builder setOnVisibleListener(@Nullable OnVisibleListener listener) {
public @NonNull Builder setOnVisibleListener(@Nullable EventListener listener) {
this.onVisibleListener = listener;
return this;
}
@ -157,11 +155,7 @@ public class Megaphone {
REACTIONS, BASIC, FULLSCREEN
}
public interface OnVisibleListener {
void onVisible(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
}
public interface OnClickListener {
void onClick(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
public interface EventListener {
void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
}
}

View File

@ -11,10 +11,15 @@ public interface MegaphoneListener {
*/
void onMegaphoneNavigationRequested(@NonNull Intent intent);
/**
* When a megaphone wants to navigate to a specific intent for a request code.
*/
void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode);
/**
* When a megaphone wants to show a toast/snackbar.
*/
void onMegaphoneToastRequested(@StringRes int stringRes);
void onMegaphoneToastRequested(@NonNull String string);
/**
* When a megaphone has been snoozed via "remind me later" or a similar option.

View File

@ -82,21 +82,12 @@ public class MegaphoneRepository {
}
@MainThread
public void markSeen(@NonNull Megaphone megaphone) {
public void markSeen(@NonNull Event event) {
long lastSeen = System.currentTimeMillis();
executor.execute(() -> {
Event event = megaphone.getEvent();
MegaphoneRecord record = getRecord(event);
if (megaphone.getMaxAppearances() != Megaphone.UNLIMITED &&
record.getSeenCount() + 1 >= megaphone.getMaxAppearances())
{
database.markFinished(event);
} else {
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
}
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
enabled = false;
resetDatabaseCache();
});

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.megaphone;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -11,26 +10,29 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Creating a new megaphone:
* - Add an enum to {@link Event}
* - Return a megaphone in {@link #forRecord(MegaphoneRecord)}
* - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)}
* - Include the event in {@link #buildDisplayOrder()}
*
* Common patterns:
* - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}.
* - For events guarded by feature flags, set a {@link ForeverSchedule} with false in
* {@link #buildDisplayOrder()}.
* - For events that change, return different megaphones in {@link #forRecord(MegaphoneRecord)}
* - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)}
* based on whatever properties you're interested in.
*/
public final class Megaphones {
@ -49,7 +51,7 @@ public final class Megaphones {
})
.map(Map.Entry::getKey)
.map(records::get)
.map(Megaphones::forRecord)
.map(record -> Megaphones.forRecord(context, record))
.toList();
boolean hasOptional = Stream.of(megaphones).anyMatch(m -> !m.isMandatory());
@ -73,13 +75,16 @@ public final class Megaphones {
private static Map<Event, MegaphoneSchedule> buildDisplayOrder() {
return new LinkedHashMap<Event, MegaphoneSchedule>() {{
put(Event.REACTIONS, new ForeverSchedule(FeatureFlags.reactionSending()));
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
}};
}
private static @NonNull Megaphone forRecord(@NonNull MegaphoneRecord record) {
private static @NonNull Megaphone forRecord(@NonNull Context context, @NonNull MegaphoneRecord record) {
switch (record.getEvent()) {
case REACTIONS:
return buildReactionsMegaphone();
case PINS_FOR_ALL:
return buildPinsForAllMegaphone(context, record);
default:
throw new IllegalArgumentException("Event not handled!");
}
@ -87,13 +92,65 @@ public final class Megaphones {
private static @NonNull Megaphone buildReactionsMegaphone() {
return new Megaphone.Builder(Event.REACTIONS, Megaphone.Style.REACTIONS)
.setMaxAppearances(Megaphone.UNLIMITED)
.setMandatory(false)
.build();
}
private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull Context context, @NonNull MegaphoneRecord record) {
if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) {
return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN)
.setMandatory(true)
.enableSnooze(null)
.setOnVisibleListener((megaphone, listener) -> {
if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) {
listener.onMegaphoneNavigationRequested(KbsMigrationActivity.createIntent(), KbsMigrationActivity.REQUEST_NEW_PIN);
}
})
.build();
} else {
Megaphone.Builder builder = new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.BASIC)
.setMandatory(true)
.setImage(R.drawable.kbs_pin_megaphone);
long daysRemaining = PinsForAllSchedule.getDaysRemaining(record.getFirstVisible(), System.currentTimeMillis());
if (PinUtil.userHasPin(ApplicationDependencies.getApplication())) {
return buildPinsForAllMegaphoneForUserWithPin(
builder.enableSnooze((megaphone, listener) -> listener.onMegaphoneToastRequested(context.getString(R.string.KbsMegaphone__well_remind_you_later_confirming_your_pin, daysRemaining)))
);
} else {
return buildPinsForAllMegaphoneForUserWithoutPin(
builder.enableSnooze((megaphone, listener) -> listener.onMegaphoneToastRequested(context.getString(R.string.KbsMegaphone__well_remind_you_later_creating_a_pin, daysRemaining)))
);
}
}
}
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithPin(@NonNull Megaphone.Builder builder) {
return builder.setTitle(R.string.KbsMegaphone__introducing_pins)
.setBody(R.string.KbsMegaphone__your_registration_lock_is_now_called_a_pin)
.setButtonText(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> {
Intent intent = CreateKbsPinActivity.getIntentForPinUpdate(ApplicationDependencies.getApplication());
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
})
.build();
}
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithoutPin(@NonNull Megaphone.Builder builder) {
return builder.setTitle(R.string.KbsMegaphone__create_a_pin)
.setBody(R.string.KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account)
.setButtonText(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> {
Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication());
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
})
.build();
}
public enum Event {
REACTIONS("reactions");
REACTIONS("reactions"),
PINS_FOR_ALL("pins_for_all");
private final String key;

View File

@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.megaphone;
import androidx.annotation.VisibleForTesting;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import java.util.concurrent.TimeUnit;
class PinsForAllSchedule implements MegaphoneSchedule {
@VisibleForTesting
static final long DAYS_UNTIL_FULLSCREEN = 8L;
@VisibleForTesting
static final long DAYS_REMAINING_MAX = DAYS_UNTIL_FULLSCREEN - 1;
private final MegaphoneSchedule schedule = new RecurringSchedule(TimeUnit.DAYS.toMillis(2));
private final boolean enabled = !SignalStore.registrationValues().isPinRequired() || FeatureFlags.pinsForAll();
static boolean shouldDisplayFullScreen(long firstVisible, long currentTime) {
if (firstVisible == 0L) {
return false;
} else {
return currentTime - firstVisible >= TimeUnit.DAYS.toMillis(DAYS_UNTIL_FULLSCREEN);
}
}
static long getDaysRemaining(long firstVisible, long currentTime) {
if (firstVisible == 0L) {
return DAYS_REMAINING_MAX;
} else {
return Util.clamp(DAYS_REMAINING_MAX - TimeUnit.MILLISECONDS.toDays(currentTime - firstVisible), 0, DAYS_REMAINING_MAX);
}
}
@Override
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
if (!enabled) return false;
if (shouldDisplayFullScreen(firstVisible, currentTime)) {
return true;
} else {
return schedule.shouldDisplay(seenCount, lastSeen, firstVisible, currentTime);
}
}
}

View File

@ -20,6 +20,7 @@ import androidx.preference.Preference;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import com.google.firebase.iid.FirebaseInstanceId;
@ -174,6 +175,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
break;
case SUCCESS:
TextSecurePreferences.setPushRegistered(getActivity(), false);
SignalStore.registrationValues().clearRegistrationComplete();
initializePushMessagingToggle();
break;
}

View File

@ -11,6 +11,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference;
import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.BlockedContactsActivity;
@ -23,8 +25,11 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@ -45,11 +50,19 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
regLock.setChecked(
TextSecurePreferences.isV1RegistrationLockEnabled(requireContext()) || SignalStore.kbsValues().isV2RegistrationLockEnabled()
);
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
Preference kbsPinChange = this.findPreference(TextSecurePreferences.KBS_PIN_CHANGE);
Preference regGroup = this.findPreference("prefs_lock_v1");
Preference kbsGroup = this.findPreference("prefs_lock_v2");
if (FeatureFlags.pinsForAll()) {
regGroup.setVisible(false);
kbsPinChange.setOnPreferenceClickListener(new KbsPinChangeListener());
} else {
kbsGroup.setVisible(false);
regLock.setChecked(PinUtil.userHasPin(requireContext()));
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
}
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
@ -84,6 +97,13 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
disablePassphrase.setChecked(!TextSecurePreferences.isPasswordDisabled(getActivity()));
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
}
}
private void initializePassphraseTimeoutSummary() {
int timeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(getActivity());
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF)
@ -151,12 +171,20 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
}
}
private class KbsPinChangeListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivityForResult(CreateKbsPinActivity.getIntentForPinUpdate(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN);
return true;
}
}
private class AccountLockClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
Context context = requireContext();
if (TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
if (PinUtil.userHasPin(context)) {
RegistrationLockDialog.showRegistrationUnlockPrompt(context, (SwitchPreferenceCompat)preference);
} else {
RegistrationLockDialog.showRegistrationLockPrompt(context, (SwitchPreferenceCompat)preference);
@ -222,7 +250,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off);
boolean registrationLockEnabled = TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
boolean registrationLockEnabled = PinUtil.userHasPin(context);
if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) {
if (registrationLockEnabled) {

View File

@ -35,6 +35,7 @@ import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
@ -321,6 +322,9 @@ public class EditProfileFragment extends Fragment {
Log.w(TAG, "Failed to delete capture file " + captureFile);
}
}
SignalStore.registrationValues().setRegistrationComplete();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
else handleFinishedLegacy();
} else {

View File

@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
public class AccountLockedFragment extends Fragment {
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.account_locked_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
AccountLockedFragmentArgs args = AccountLockedFragmentArgs.fromBundle(requireArguments());
TextView description = view.findViewById(R.id.account_locked_description);
description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, args.getTimeRemaining()));
view.findViewById(R.id.account_locked_next).setOnClickListener(this::onNextClicked);
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(this::onLearnMoreClicked);
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
onNext();
}
});
}
private void onNextClicked(@NonNull View unused) {
onNext();
}
private void onLearnMoreClicked(@NonNull View unused) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
startActivity(intent);
}
private void onNext() {
requireActivity().finish();
}
}

View File

@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
@ -126,8 +127,13 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
if (FeatureFlags.pinsForAll()) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
} else {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
}
}
});
}

View File

@ -0,0 +1,221 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
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 android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.util.concurrent.TimeUnit;
public final class KbsLockFragment extends BaseRegistrationFragment {
private EditText pinEntry;
private CircularProgressButton pinButton;
private TextView errorLabel;
private TextView keyboardToggle;
private long timeRemaining;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.kbs_lock_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
View pinForgotButton = view.findViewById(R.id.kbs_lock_forgot_pin);
timeRemaining = KbsLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining();
pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining));
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
handlePinEntry();
return true;
}
return false;
});
pinButton.setOnClickListener((v) -> {
hideKeyboard(requireContext(), pinEntry);
handlePinEntry();
});
keyboardToggle.setOnClickListener((v) -> {
KbsKeyboardType keyboardType = getPinEntryKeyboardType();
updateKeyboard(keyboardType);
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
});
RegistrationViewModel model = getModel();
model.getTokenResponseCredentialsPair().observe(getViewLifecycleOwner(), pair -> {
if (pair.first().getTries() == 0) {
lockAccount();
}
});
model.onRegistrationLockFragmentCreate();
}
private KbsKeyboardType getPinEntryKeyboardType() {
boolean isNumeric = (pinEntry.getImeOptions() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
return isNumeric ? KbsKeyboardType.NUMERIC : KbsKeyboardType.ALPHA_NUMERIC;
}
private void handlePinEntry() {
final String pin = pinEntry.getText().toString();
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
return;
}
RegistrationViewModel model = getModel();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
String storageCredentials = model.getBasicStorageCredentials();
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
setSpinning(pinButton);
registrationService.verifyAccount(requireActivity(),
model.getFcmToken(),
model.getTextCodeEntered(),
pin, storageCredentials, tokenResponse,
new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
cancelSpinning(pinButton);
SignalStore.kbsValues().setKeyboardType(getPinEntryKeyboardType());
Navigation.findNavController(requireView()).navigate(KbsLockFragmentDirections.actionSuccessfulRegistration());
}
@Override
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
model.setStorageCredentials(storageCredentials);
cancelSpinning(pinButton);
pinEntry.setText("");
errorLabel.setText(R.string.KbsLockFragment__incorrect_pin);
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
cancelSpinning(pinButton);
model.setKeyBackupCurrentToken(tokenResponse);
int triesRemaining = tokenResponse.getTries();
if (triesRemaining == 0) {
lockAccount();
return;
}
if (triesRemaining == 3) {
long daysRemaining = getLockoutDays(timeRemaining);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.KbsLockFragment__incorrect_pin)
.setMessage(getString(R.string.KbsLockFragment__you_have_d_attempts_remaining, triesRemaining, daysRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.show();
}
if (triesRemaining > 5) {
errorLabel.setText(R.string.KbsLockFragment__incorrect_pin_try_again);
} else {
errorLabel.setText(getString(R.string.KbsLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining));
}
}
@Override
public void onTooManyAttempts() {
cancelSpinning(pinButton);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
}
@Override
public void onError() {
cancelSpinning(pinButton);
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
});
}
private void handleForgottenPin(long timeRemainingMs) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.KbsLockFragment__forgot_your_pin)
.setMessage(getString(R.string.KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, getLockoutDays(timeRemainingMs)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
private long getLockoutDays(long timeRemainingMs) {
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
}
private void lockAccount() {
KbsLockFragmentDirections.ActionAccountLocked action = KbsLockFragmentDirections.actionAccountLocked(timeRemaining);
Navigation.findNavController(requireView()).navigate(action);
}
private void updateKeyboard(@NonNull KbsKeyboardType keyboard) {
boolean isAlphaNumeric = keyboard == KbsKeyboardType.ALPHA_NUMERIC;
pinEntry.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.KbsLockFragment__enter_alphanumeric_pin;
} else {
return R.string.KbsLockFragment__enter_numeric_pin;
}
}
}

View File

@ -14,7 +14,11 @@ import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.PinUtil;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.util.FeatureFlags;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@ -30,12 +34,19 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
FragmentActivity activity = requireActivity();
if (!isReregister()) {
Intent setProfileNameIntent = getRoutedIntent(activity, EditProfileActivity.class, new Intent(activity, MainActivity.class));
final Intent main = new Intent(activity, MainActivity.class);
final Intent next = getRoutedIntent(activity, EditProfileActivity.class, main);
setProfileNameIntent.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
next.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
activity.startActivity(setProfileNameIntent);
Context context = requireContext();
if (FeatureFlags.pinsForAll() && !PinUtil.userHasPin(context)) {
activity.startActivity(getRoutedIntent(activity, CreateKbsPinActivity.class, next));
} else {
activity.startActivity(next);
}
}
activity.finish();

View File

@ -52,7 +52,7 @@ public final class FeatureFlags {
private static final String USERNAMES = generateKey("usernames");
private static final String KBS = generateKey("kbs");
private static final String STORAGE_SERVICE = generateKey("storageService");
private static final String REACTION_SENDING = generateKey("reactionSending");
private static final String PINS_FOR_ALL = generateKey("beta.pinsForAll"); // TODO [alex] remove beta prefix
/**
* Values in this map will take precedence over any value. If you do not wish to have any sort of
@ -82,6 +82,7 @@ public final class FeatureFlags {
* Flags in this set will stay true forever once they receive a true value from a remote config.
*/
private static final Set<String> STICKY = Sets.newHashSet(
PINS_FOR_ALL // TODO [alex] -- add android.beta.pinsForAll to sticky set when we remove prefix
);
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>();
@ -153,9 +154,9 @@ public final class FeatureFlags {
return value;
}
/** Send support for reactions. */
public static synchronized boolean reactionSending() {
return getValue(REACTION_SENDING, false);
/** Enables new KBS UI and notices but does not require user to set a pin */
public static boolean pinsForAll() {
return SignalStore.registrationValues().pinWasRequiredAtRegistration() || getValue(PINS_FOR_ALL, false);
}
/** Only for rendering debug info. */

View File

@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.util;
public final class RequestCodes {
public static final int NOT_SET = -1;
private RequestCodes() { }
}

View File

@ -17,6 +17,7 @@ import org.greenrobot.eventbus.EventBus;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.profiles.ProfileName;
@ -163,6 +164,8 @@ public class TextSecurePreferences {
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME_POST_KBS = "pref_registration_lock_last_reminder_time_post_kbs";
private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval";
public static final String KBS_PIN_CHANGE = "pref_kbs_change";
private static final String SERVICE_OUTAGE = "pref_service_outage";
private static final String LAST_OUTAGE_CHECK_TIME = "pref_last_outage_check_time";

View File

@ -509,6 +509,10 @@ public class Util {
return Math.min(Math.max(value, min), max);
}
public static long clamp(long value, long min, long max) {
return Math.min(Math.max(value, min), max);
}
public static float clamp(float value, float min, float max) {
return Math.min(Math.max(value, min), max);
}

View File

@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.util.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.snackbar.Snackbar;
public class SlideUpWithSnackbarBehavior extends CoordinatorLayout.Behavior<View> {
public SlideUpWithSnackbarBehavior(@NonNull Context context, @Nullable AttributeSet attributeSet) {
super(context, attributeSet);
}
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent,
@NonNull View child,
@NonNull View dependency)
{
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
child.setTranslationY(translationY);
return true;
}
@Override
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent,
@NonNull View child,
@NonNull View dependency)
{
child.setTranslationY(0);
}
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent,
@NonNull View child,
@NonNull View dependency)
{
return dependency instanceof Snackbar.SnackbarLayout;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/account_locked_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="49dp"
android:gravity="center_horizontal"
android:text="@string/AccountLockedFragment__account_locked"
android:textAppearance="@style/TextAppearance.Signal.Title1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/account_locked_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="27dp"
android:gravity="center_horizontal"
android:minHeight="66dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/core_grey_60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/account_locked_title" />
<Button
android:id="@+id/account_locked_next"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:background="@drawable/cta_button_background"
android:text="@string/AccountLockedFragment__next"
android:textColor="@color/core_white"
app:layout_constraintBottom_toTopOf="@id/account_locked_learn_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/account_locked_learn_more"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:text="@string/AccountLockedFragment__learn_more"
android:textColor="@color/signal_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/edit_kbs_pin_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Signal.Title1"
app:layout_constraintBottom_toTopOf="@id/edit_kbs_pin_keyboard_toggle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.20"
tools:text="Create your PIN" />
<TextView
android:id="@+id/edit_kbs_pin_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="27dp"
android:gravity="center_horizontal"
android:minHeight="66dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?attr/title_text_color_secondary"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_title"
tools:text="PINs add an extra layer of security to your account. Write your PIN down and keep it in a safe place. It can\'t be recovered." />
<EditText
android:id="@+id/edit_kbs_pin_input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:inputType="numberPassword"
android:minWidth="210dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_description" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/edit_kbs_pin_lottie_progress"
android:layout_width="57dp"
android:layout_height="57dp"
android:layout_gravity="center_horizontal"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_description" />
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/edit_kbs_pin_lottie_end"
android:layout_width="57dp"
android:layout_height="57dp"
android:layout_gravity="center_horizontal"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_description" />
<TextView
android:id="@+id/edit_kbs_pin_input_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_input"
app:layout_goneMarginTop="65dp"
tools:text="PIN must be at least 6 digits" />
<Button
android:id="@+id/edit_kbs_pin_keyboard_toggle"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:textColor="@color/signal_primary"
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_input_label"
app:layout_constraintBottom_toTopOf="@id/edit_kbs_pin_confirm"
app:layout_constraintVertical_bias="1.0"
tools:text="Create Alphanumeric Pin" />
<Button
android:id="@+id/edit_kbs_pin_confirm"
style="@style/Button.Primary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:background="@drawable/cta_button_background"
android:text="@string/BaseKbsPinFragment__next"
android:textColor="@color/core_white"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -172,48 +172,63 @@
tools:listitem="@layout/conversation_list_item_view"
tools:visibility="gone" />
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/camera_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:contentDescription="@string/conversation_list_fragment__open_camera_description"
android:focusable="true"
android:tint="?conversation_list_camera_icon_tint"
app:backgroundTint="?conversation_list_camera_button_background"
app:layout_constraintBottom_toTopOf="@id/fab"
app:layout_constraintEnd_toEndOf="@id/fab"
app:srcCompat="@drawable/ic_camera_solid_24" />
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
android:focusable="true"
android:tint="?conversation_list_compose_icon_tint"
app:layout_constraintBottom_toTopOf="@id/megaphone_container"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_compose_solid_24" />
<androidx.cardview.widget.CardView
android:id="@+id/megaphone_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clipToPadding="false"
android:clipChildren="false"
android:visibility="gone"
app:contentPadding="0dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:cardBackgroundColor="?megaphone_background"
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="500dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior=".util.views.SlideUpWithSnackbarBehavior">
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/camera_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:contentDescription="@string/conversation_list_fragment__open_camera_description"
android:focusable="true"
android:tint="?conversation_list_camera_icon_tint"
app:backgroundTint="?conversation_list_camera_button_background"
app:layout_constraintBottom_toTopOf="@id/fab"
app:layout_constraintEnd_toEndOf="@id/fab"
app:srcCompat="@drawable/ic_camera_solid_24" />
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
android:focusable="true"
android:tint="?conversation_list_compose_icon_tint"
app:layout_constraintBottom_toTopOf="@id/megaphone_container"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_compose_solid_24" />
<androidx.cardview.widget.CardView
android:id="@+id/megaphone_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:clipChildren="false"
android:clipToPadding="false"
android:visibility="gone"
app:cardBackgroundColor="?megaphone_background"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:contentPadding="0dp"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".lock.v2.CreateKbsPinActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/create_kbs_pin" />
</FrameLayout>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/kbs_lock_pin_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/KbsLockFragment__enter_your_pin"
android:textAppearance="@style/TextAppearance.Signal.Title1"
app:layout_constraintBottom_toTopOf="@id/kbs_lock_keyboard_toggle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.20" />
<TextView
android:id="@+id/kbs_lock_pin_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="27dp"
android:gravity="center_horizontal"
android:minHeight="66dp"
android:text="@string/KbsLockFragment__enter_the_pin_you_created"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/core_grey_60"
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_title" />
<EditText
android:id="@+id/kbs_lock_pin_input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
android:inputType="numberPassword"
android:minWidth="210dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_description" />
<TextView
android:id="@+id/kbs_lock_pin_input_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_input"
tools:text="@string/KbsLockFragment__incorrect_pin_try_again" />
<Button
android:id="@+id/kbs_lock_forgot_pin"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:text="@string/KbsLockFragment__forgot_pin"
android:textColor="@color/signal_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_input" />
<Button
android:id="@+id/kbs_lock_keyboard_toggle"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:textColor="@color/signal_primary"
app:layout_constraintBottom_toTopOf="@id/kbs_lock_pin_confirm"
tools:text="Create Alphanumeric Pin" />
<com.dd.CircularProgressButton
android:id="@+id/kbs_lock_pin_confirm"
style="@style/Button.Registration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
app:cpb_textIdle="@string/RegistrationActivity_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".lock.v2.KbsMigrationActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/kbs_migration" />
</FrameLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/preferences_app_protection__change" />

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/header_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/signal_primary"
android:gravity="center"
android:orientation="horizontal"
android:padding="40dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/KbsReminderDialog__enter_your_signal_pin"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pin_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="80dp"
android:paddingTop="40dp"
android:paddingEnd="80dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/registration_lock_reminder_view_pin_text_color" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/reminder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.3"
android:paddingStart="20dp"
android:paddingTop="40dp"
android:paddingEnd="20dp"
android:paddingBottom="40dp"
android:textSize="15sp"
tools:text="@string/KbsReminderDialog__to_help_you_memorize_your_pin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
<Button
android:id="@+id/skip"
style="@style/Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/KbsReminderDialog__skip" />
<Button
android:id="@+id/submit"
style="@style/Button.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/KbsReminderDialog__submit" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/kbs_splash_image"
android:layout_width="0dp"
android:layout_height="0dp"
app:srcCompat="?attr/kbs_splash_image"
app:layout_constraintBottom_toTopOf="@id/kbs_splash_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/kbs_splash_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="7dp"
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.Signal.Title1"
app:layout_constraintBottom_toTopOf="@id/kbs_splash_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/KbsSplashFragment__registration_lock_equals_pin" />
<TextView
android:id="@+id/kbs_splash_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="27dp"
android:layout_marginEnd="27dp"
android:layout_marginBottom="36dp"
android:gravity="center_horizontal"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/core_grey_60"
app:layout_constraintBottom_toTopOf="@id/kbs_splash_primary_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/KbsSplashFragment__your_registration_lock_is_now_called_a_pin" />
<Button
android:id="@+id/kbs_splash_primary_action"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:background="@drawable/cta_button_background"
android:text="@string/KbsSplashFragment__create_your_pin"
android:textColor="@color/core_white"
app:layout_constraintBottom_toTopOf="@id/kbs_splash_secondary_action"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/kbs_splash_secondary_action"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="16dp"
android:text="@string/KbsSplashFragment__learn_more"
android:textColor="@color/signal_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/create_kbs_pin"
app:startDestination="@id/createKbsPinFragment">
<fragment
android:id="@+id/createKbsPinFragment"
android:name="org.thoughtcrime.securesms.lock.v2.CreateKbsPinFragment"
android:label="fragment_edit_kbs_pin"
tools:layout="@layout/base_kbs_pin_fragment">
<action
android:id="@+id/action_confirmPin"
app:destination="@id/confirmKbsPinFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<argument
app:argType="boolean"
android:name="is_new_pin"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/confirmKbsPinFragment"
android:name="org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinFragment"
android:label="fragment_confirm_new_pin"
tools:layout="@layout/base_kbs_pin_fragment">
<argument
app:argType="boolean"
android:name="is_new_pin"
android:defaultValue="false" />
<argument
android:name="user_entry"
android:defaultValue="@null"
app:argType="org.thoughtcrime.securesms.lock.v2.KbsPin"
app:nullable="true" />
<argument
android:name="keyboard"
android:defaultValue="NUMERIC"
app:argType="org.thoughtcrime.securesms.lock.v2.KbsKeyboardType"
app:nullable="false" />
</fragment>
</navigation>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/kbs_pin_migration"
app:startDestination="@id/kbsSplashFragment">
<fragment
android:id="@+id/kbsSplashFragment"
android:name="org.thoughtcrime.securesms.lock.v2.KbsSplashFragment"
android:label="fragment_kbs_splash"
tools:layout="@layout/kbs_splash_fragment">
<action
android:id="@+id/action_createKbsPin"
app:destination="@id/create_kbs_pin"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<include app:graph="@navigation/create_kbs_pin" />
</navigation>

View File

@ -91,6 +91,16 @@
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_requireKbsLockPin"
app:destination="@id/kbsLockFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_wrongNumber"
app:popUpTo="@id/enterCodeFragment"
@ -134,6 +144,49 @@
</fragment>
<fragment
android:id="@+id/kbsLockFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.KbsLockFragment"
android:label="fragment_kbs_lock"
tools:layout="@layout/kbs_lock_fragment">
<action
android:id="@+id/action_successfulRegistration"
app:destination="@id/registrationCompletePlaceHolderFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_accountLocked"
app:destination="@id/accountLockedFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim"
app:popUpTo="@+id/welcomeFragment"
app:popUpToInclusive="true" />
<argument
android:name="timeRemaining"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/accountLockedFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
android:label="fragment_account_locked"
tools:layout="@layout/account_locked_fragment">
<argument
android:name="timeRemaining"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/captchaFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -31,6 +31,8 @@
<attr name="conversation_list_compose_icon_tint" format="color" />
<attr name="conversation_list_camera_button_background" format="color"/>
<attr name="kbs_splash_image" format="reference" />
<attr name="conversation_sent_card_background" format="reference|color"/>
<attr name="conversation_group_member_name" format="reference|color"/>
<attr name="conversation_received_card_background" format="reference|color"/>

View File

@ -1682,12 +1682,18 @@
<string name="BaseKbsPinFragment__create_numeric_pin">Create numeric PIN</string>
<!-- CreateKbsPinFragment -->
<string name="CreateKbsPinFragment__pin_must_be_at_least_characters">PIN must be at least %1$d characters</string>
<string name="CreateKbsPinFragment__pin_must_be_at_least_digits">PIN must be at least %1$d digits</string>
<plurals name="CreateKbsPinFragment__pin_must_be_at_least_characters">
<item quantity="one">PIN must be at least %1$d character</item>
<item quantity="other">PIN must be at least %1$d characters</item>
</plurals>
<plurals name="CreateKbsPinFragment__pin_must_be_at_least_digits">
<item quantity="one">PIN must be at least %1$d digit</item>
<item quantity="other">PIN must be at least %1$d digits</item>
</plurals>
<string name="CreateKbsPinFragment__create_a_new_pin">Create a new PIN</string>
<string name="CreateKbsPinFragment__because_youre_still_logged_in">Because you\'re still logged in, you can create a new PIN. When you\'re logged out, there is no way to recover your PIN.</string>
<string name="CreateKbsPinFragment__create_your_pin">Create your PIN</string>
<string name="CreateKbsPinFragment__pins_add_an_extra_layer_of_security">PINs add an extra layer of security to your account. Write your PIN down and keep it in a safe place. It can\'t be recovered.</string>
<string name="CreateKbsPinFragment__pins_add_an_extra_layer_of_security">PINs add an extra layer of security to your account. It\'s important to remember this PIN, as it can\'t be recovered.</string>
<!-- ConfirmKbsPinFragment -->
<string name="ConfirmKbsPinFragment__pins_dont_match">PINs don\'t match. Try again.</string>
@ -1695,18 +1701,19 @@
<string name="ConfirmKbsPinFragment__pin_creation_failed">PIN creation failed</string>
<string name="ConfirmKbsPinFragment__your_pin_was_not_saved">Your PIN was not saved. We\'ll prompt you to create a PIN later.</string>
<string name="ConfirmKbsPinFragment__pin_created">PIN created.</string>
<string name="ConfirmKbsPinFragment__re_enter_pin">Re-enter PIN</string>
<string name="ConfirmKbsPinFragment__creating_pin">Creating PIN...</string>
<!-- KbsSplashFragment -->
<string name="KbsSplashFragment__introducing_pins">Introducing PINs</string>
<string name="KbsSplashFragment__add_another_level_of_security_to_your_account">Add another level of security to your account. %1$s</string>
<string name="KbsSplashFragment__read_more_here">Read more here.</string>
<string name="KbsSplashFragment__read_more_link">https://signal.org/blog/secure-value-recovery/</string>
<string name="KbsSplashFragment__pins_add_another_level_of_security_to_your_account">PINs add another level of security to your account. Create one now.</string>
<string name="KbsSplashFragment__learn_more">Learn More</string>
<string name="KbsSplashFragment__learn_more_link">https://signal.org/blog/secure-value-recovery/</string>
<string name="KbsSplashFragment__registration_lock_equals_pin">Registration Lock = PIN</string>
<string name="KbsSplashFragment__your_registration_lock_is_now_called_a_pin">Your Registration Lock is now called a PIN, and it does more. Update it now. %1$s</string>
<string name="KbsSplashFragment__your_registration_lock_is_now_called_a_pin">Your Registration Lock is now called a PIN, and it does more. Update it now.</string>
<string name="KbsSplashFragment__read_more_about_pins">Read more about PINs.</string>
<string name="KbsSplashFragment__update_pin">Update PIN</string>
<string name="KbsSplashFragment__create_your_pin">Create your PIN</string>
<string name="KbsSplashFragment__remind_me_later">Remind me later</string>
<!-- KBS Reminder Dialog -->
<string name="KbsReminderDialog__enter_your_signal_pin">Enter your Signal PIN</string>
@ -1735,7 +1742,17 @@
<string name="KbsLockFragment__incorrect_pin">Incorrect PIN</string>
<string name="KbsLockFragment__you_have_d_attempts_remaining">You have %1$d attempts remaining. If you run out of attempts your account will be locked for %2$d days. After %3$d days of inactivity, you can re-register without your PIN. Your account will be wiped and all content deleted.</string>
<string name="KbsLockFragment__forgot_your_pin">Forgot your PIN?</string>
<string name="KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover">For your privacy and security, there is no way to recover your PIN. If you run out of attempts, you can re-verify with SMS after %1$d days of inactivity. In this case, your account will be wiped and all content deleted.</string>
<string name="KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover">For your privacy and security, there is no way to recover your PIN. If you can\'t remember your PIN, you can re-verify with SMS after %1$d days of inactivity. In this case, your account will be wiped and all content deleted.</string>
<!-- KBS Megaphone -->
<string name="KbsMegaphone__create_a_pin">Create a PIN</string>
<string name="KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account">PINs add another layer of security to your Signal account.</string>
<string name="KbsMegaphone__create_pin">Create PIN</string>
<string name="KbsMegaphone__introducing_pins">Introducing PINs</string>
<string name="KbsMegaphone__your_registration_lock_is_now_called_a_pin">Your Registration Lock is now called a PIN. Updating it takes seconds.</string>
<string name="KbsMegaphone__update_pin">Update PIN</string>
<string name="KbsMegaphone__well_remind_you_later_creating_a_pin">We\'ll remind you later. Creating a PIN will become mandatory in %1$d days.</string>
<string name="KbsMegaphone__well_remind_you_later_confirming_your_pin">We\'ll remind you later. Confirming your PIN will become mandatory in %1$d days.</string>
<!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string>

View File

@ -45,6 +45,12 @@
<item name="android:textColor">@color/core_white</item>
</style>
<style name="TextAppearance.Signal.Title1" parent="@style/TextAppearance.AppCompat.Title">
<item name="android:textStyle">bold</item>
<item name="android:textSize">28sp</item>
<item name="android:color">?attr/title_text_color_primary</item>
</style>
<style name="TextAppearance.Signal.Title2" parent="@style/TextAppearance.AppCompat.Title">
<item name="android:textStyle">bold</item>
</style>

View File

@ -188,6 +188,8 @@
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
<item name="kbs_splash_image">@drawable/ic_kbs_splash_light_svg</item>
<item name="attachment_type_selector_background">@color/white</item>
<item name="attachment_document_icon_small">@drawable/ic_document_small_light</item>
<item name="attachment_document_icon_large">@drawable/ic_document_large_light</item>
@ -440,6 +442,8 @@
<item name="homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
<item name="kbs_splash_image">@drawable/ic_kbs_splash_dark_svg</item>
<item name="attachment_type_selector_background">@color/core_grey_95</item>
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>
<item name="attachment_document_icon_large">@drawable/ic_document_large_dark</item>
@ -651,6 +655,15 @@
<item name="android:windowBackground">@drawable/permission_rationale_dialog_corners</item>
</style>
<style name="RationaleDialogLight.SignalAccent">
<item name="colorAccent">@color/signal_primary</item>
</style>
<style name="RationaleDialogDark.SignalAccent">
<item name="colorAccent">@color/signal_primary</item>
</style>
<style name="Theme.Signal.Insights.Modal" parent="@style/Theme.AppCompat.Dialog.MinWidth">
</style>

View File

@ -102,7 +102,7 @@
<PreferenceCategory android:layout="@layout/preference_divider"/>
<PreferenceCategory android:title="@string/preferences_app_protection__registration_lock">
<PreferenceCategory android:key="prefs_lock_v1" android:title="@string/preferences_app_protection__registration_lock">
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_registration_lock"
@ -110,5 +110,12 @@
android:summary="@string/preferences_app_protection__enable_a_registration_lock_pin_that_will_be_required"/>
</PreferenceCategory>
<PreferenceCategory android:key="prefs_lock_v2" android:title="@string/preferences_app_protection__signal_pin">
<Preference
android:widgetLayout="@layout/kbs_pin_change_preference"
android:key="pref_kbs_change"
android:title="@string/preferences_app_protection__pin"
android:summary="@string/preferences_app_protection__your_pin_adds_an_extra_layer_of_security_and_backs" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,219 @@
package org.thoughtcrime.securesms.megaphone;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.thoughtcrime.securesms.keyvalue.RegistrationValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FeatureFlags;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({SignalStore.class, FeatureFlags.class, RegistrationValues.class})
public class PinsForAllScheduleTest {
private final PinsForAllSchedule testSubject = new PinsForAllSchedule();
private final RegistrationValues registrationValues = mock(RegistrationValues.class);
@Before
public void setUp() {
mockStatic(SignalStore.class);
mockStatic(FeatureFlags.class);
when(SignalStore.registrationValues()).thenReturn(registrationValues);
}
@Test
public void givenFirstVisibleIsZero_whenIShouldDisplayFullscreen_thenIExpectFalse() {
// GIVEN
long firstVisible = 0;
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(firstVisible, 0);
// THEN
assertFalse(result);
}
@Test
public void givenFirstVisibleIsNow_whenIShouldDisplayFullscreen_thenIExpectFalse() {
// GIVEN
long now = System.currentTimeMillis();
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(now, now);
// THEN
assertFalse(result);
}
@Test
public void givenFirstVisibleIsHalfFullscreenTimeout_whenIShouldDisplayFullscreen_thenIExpectFalse() {
// GIVEN
long now = System.currentTimeMillis();
long lastWeek = now - TimeUnit.DAYS.toMillis(PinsForAllSchedule.DAYS_UNTIL_FULLSCREEN / 2);
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(lastWeek, now);
// THEN
assertFalse(result);
}
@Test
public void givenFirstVisibleIsFullscreenTimeout_whenIShouldDisplayFullscreen_thenIExpectTrue() {
// GIVEN
long now = System.currentTimeMillis();
long lastWeek = now - TimeUnit.DAYS.toMillis(PinsForAllSchedule.DAYS_UNTIL_FULLSCREEN);
// WHEN
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(lastWeek, now);
// THEN
assertTrue(result);
}
@Test
public void givenFirstVisibleIsZero_whenIGetDaysRemaining_thenIExpectMax() {
// GIVEN
long firstVisible = 0;
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX;
// WHEN
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, 0);
// THEN
assertEquals(expected, result);
}
@Test
public void givenFirstVisibleIsNow_whenIGetDaysRemaining_thenIExpectMax() {
// GIVEN
long now = System.currentTimeMillis();
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX;
// WHEN
long result = PinsForAllSchedule.getDaysRemaining(now, now);
// THEN
assertEquals(expected, result);
}
@Test
public void givenFirstVisibleIsFiveSecondsAgo_whenIGetDaysRemaining_thenIExpectMax() {
// GIVEN
long now = System.currentTimeMillis();
long firstVisible = now - TimeUnit.SECONDS.toMillis(5);
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX;
// WHEN
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, now);
// THEN
assertEquals(expected, result);
}
@Test
public void givenFirstVisibleIsADayAgo_whenIGetDaysRemaining_thenIExpectMaxLessOne() {
// GIVEN
long now = System.currentTimeMillis();
long firstVisible = now - TimeUnit.DAYS.toMillis(1);
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX - 1;
// WHEN
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, now);
// THEN
assertEquals(expected, result);
}
@Test
public void givenFirstVisibleIsAMonthAgo_whenIGetDaysRemaining_thenIExpectZero() {
// GIVEN
long now = System.currentTimeMillis();
long firstVisible = now - TimeUnit.DAYS.toMillis(31);
long expected = 0;
// WHEN
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, now);
// THEN
assertEquals(expected, result);
}
@Test
public void whenUserIsANewInstallAndFlagIsDisabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
when(FeatureFlags.pinsForAll()).thenReturn(false);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertFalse(result);
}
@Test
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
when(FeatureFlags.pinsForAll()).thenReturn(true);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertFalse(result);
}
@Test
public void whenUserIsNotANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectTrue() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
when(FeatureFlags.pinsForAll()).thenReturn(true);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertTrue(result);
}
@Test
public void whenUserIsNotANewInstallAndFlagIsNotEnabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
when(FeatureFlags.pinsForAll()).thenReturn(false);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertFalse(result);
}
@Test
public void whenKillSwitchEnabled_whenIShouldDisplay_thenIExpectFalse() {
// GIVEN
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
when(FeatureFlags.pinsForAll()).thenReturn(true);
when(FeatureFlags.pinsForAllMegaphoneKillSwitch()).thenReturn(true);
// WHEN
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
// THEN
assertFalse(result);
}
}