Implement new PIN UX.

This commit is contained in:
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

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="42.7dp"
android:viewportWidth="360"
android:viewportHeight="427">
<path
android:pathData="M284.256,346.041C329.141,315.766 305.914,195.351 259.927,127.173C213.94,58.994 118.155,37.768 73.271,68.043C28.386,98.318 29.28,178.13 75.266,246.308C121.253,314.486 239.372,376.316 284.256,346.041Z"
android:fillColor="#2B2C2D"
android:fillType="evenOdd"/>
<path
android:pathData="M153.661,51.211C158.713,36.511 174.733,28.698 189.427,33.767L285.081,66.769C299.75,71.829 307.547,87.816 302.504,102.491L215.191,356.568C210.139,371.268 194.119,379.081 179.425,374.012L83.771,341.01C69.102,335.95 61.305,319.963 66.348,305.288L153.661,51.211Z"
android:fillColor="#747474"
android:fillType="evenOdd"/>
<path
android:pathData="M199.07,43.701L185.209,38.928C175.42,35.557 164.751,40.761 161.38,50.55L72.196,309.562C68.825,319.351 74.028,330.02 83.818,333.391L184.409,368.027C194.199,371.398 204.868,366.195 208.239,356.405L297.423,97.394C300.794,87.604 295.591,76.936 285.801,73.565L272.352,68.934L272.148,69.527C270.238,75.074 264.192,78.023 258.644,76.113L205.452,57.797C199.904,55.887 196.956,49.841 198.866,44.294L199.07,43.701Z"
android:fillColor="#363636"
android:fillType="evenOdd"/>
<path
android:pathData="M160.279,173.86L158.361,172.625L159.091,171.652L160.789,173.229L161.389,170.86L162.559,171.263L161.545,173.531L163.832,173.345L163.801,174.583L161.507,174.337L162.345,176.556L161.157,176.918L160.714,174.542L158.923,176.082L158.193,175.095L160.279,173.86ZM173.312,178.351L171.394,177.116L172.124,176.143L173.822,177.72L174.422,175.351L175.592,175.754L174.578,178.022L176.865,177.836L176.834,179.074L174.54,178.828L175.378,181.047L174.19,181.409L173.747,179.033L171.956,180.573L171.226,179.586L173.312,178.351ZM186.345,182.842L184.427,181.607L185.157,180.634L186.855,182.211L187.455,179.842L188.625,180.245L187.611,182.513L189.898,182.327L189.867,183.565L187.573,183.319L188.411,185.538L187.223,185.9L186.78,183.524L184.989,185.064L184.259,184.077L186.345,182.842ZM199.378,187.333L197.46,186.098L198.19,185.125L199.888,186.702L200.488,184.333L201.658,184.736L200.644,187.004L202.931,186.818L202.9,188.056L200.606,187.81L201.444,190.029L200.256,190.391L199.813,188.015L198.022,189.555L197.292,188.568L199.378,187.333ZM212.411,191.824L210.493,190.589L211.223,189.615L212.921,191.193L213.521,188.824L214.691,189.227L213.678,191.495L215.964,191.309L215.933,192.547L213.639,192.301L214.477,194.52L213.289,194.882L212.846,192.506L211.055,194.046L210.325,193.059L212.411,191.824ZM225.444,196.315L223.526,195.08L224.256,194.106L225.954,195.684L226.554,193.315L227.724,193.718L226.711,195.986L228.997,195.8L228.966,197.038L226.672,196.792L227.51,199.011L226.322,199.373L225.879,196.997L224.088,198.537L223.358,197.55L225.444,196.315Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,44 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="41.2dp"
android:viewportWidth="360"
android:viewportHeight="412">
<group>
<clip-path android:pathData="M-8,0h377v411.552h-377z M 0,0"/>
<path
android:pathData="M283.84,345.384C328.545,315.231 305.411,195.299 259.608,127.394C213.806,59.489 118.405,38.348 73.701,68.501C28.996,98.655 29.886,178.147 75.688,246.052C121.491,313.956 239.136,375.538 283.84,345.384Z"
android:strokeAlpha="0.760579"
android:fillColor="#D8EBFA"
android:fillType="evenOdd"
android:fillAlpha="0.760579"/>
<path
android:pathData="M153.772,51.718C158.8,37.087 174.744,29.311 189.368,34.356L284.678,67.239C299.277,72.276 307.037,88.186 302.018,102.792L215.041,355.889C210.014,370.519 194.07,378.296 179.446,373.25L84.135,340.368C69.536,335.331 61.776,319.42 66.795,304.815L153.772,51.718Z"
android:fillColor="#F2F2F2"
android:fillType="evenOdd"/>
<path
android:pathData="M198.992,44.257L185.174,39.499C175.431,36.144 164.813,41.323 161.458,51.066L72.622,309.065C69.267,318.808 74.446,329.426 84.189,332.781L184.403,367.288C194.146,370.643 204.765,365.464 208.119,355.721L296.955,97.722C300.31,87.979 295.132,77.361 285.388,74.006L271.98,69.389L271.774,69.987C269.873,75.508 263.856,78.443 258.335,76.541L205.341,58.294C199.819,56.393 196.885,50.376 198.786,44.855L198.992,44.257Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M142.531,251.916L134.043,256.955L126.667,276.331L129.465,283.601L119.591,310.688L95.268,316.218L101.109,265.665L121.09,232.022L144.796,244.074V248.681L176.661,247.262L223.199,268.301L220.565,292.54L198.435,346.36L182.383,331.438L192.682,305.122L198.435,299.877L200.421,277.414L178.651,250.288H165.372L142.531,251.916Z"
android:fillColor="#E8E8E8"
android:fillAlpha="0.01"
android:fillType="evenOdd"/>
<group>
<clip-path android:pathData="M142.531,251.916L134.043,256.955L126.667,276.331L129.465,283.601L119.591,310.688L95.268,316.218L101.109,265.665L121.09,232.022L144.796,244.074V248.681L176.661,247.262L223.199,268.301L220.565,292.54L198.435,346.36L182.383,331.438L192.682,305.122L198.435,299.877L200.421,277.414L178.651,250.288H165.372L142.531,251.916Z M 0,0"/>
<path
android:pathData="M151.889,249.45L131.601,256.264L132.67,271.55L127.988,276.988L128.888,289.856L120.682,316.955L183.106,337.45L194.187,301.799L199.322,294.51L199.836,262.966L176.641,244.987L151.889,249.45Z"
android:fillColor="#E8E8E8"
android:fillAlpha="0.01"
android:fillType="evenOdd"/>
<group>
<clip-path android:pathData="M151.889,249.45L131.601,256.264L132.67,271.55L127.988,276.988L128.888,289.856L120.682,316.955L183.106,337.45L194.187,301.799L199.322,294.51L199.836,262.966L176.641,244.987L151.889,249.45Z M 0,0"/>
<path
android:pathData="M124.227,234.022l100.912,34.747l-25.498,74.051l-100.912,-34.747z"/>
</group>
</group>
<path
android:pathData="M160.36,173.85L158.452,172.622L159.178,171.653L160.868,173.222L161.466,170.864L162.63,171.265L161.621,173.523L163.897,173.337L163.866,174.569L161.583,174.325L162.416,176.533L161.234,176.893L160.793,174.528L159.01,176.062L158.283,175.08L160.36,173.85ZM173.332,178.317L171.424,177.089L172.15,176.119L173.84,177.689L174.438,175.331L175.602,175.732L174.593,177.99L176.869,177.804L176.838,179.036L174.555,178.791L175.388,180.999L174.206,181.359L173.765,178.995L171.982,180.528L171.255,179.547L173.332,178.317ZM186.304,182.783L184.396,181.555L185.122,180.586L186.812,182.155L187.41,179.798L188.574,180.199L187.565,182.456L189.841,182.271L189.81,183.503L187.527,183.258L188.361,185.466L187.178,185.826L186.737,183.462L184.954,184.995L184.227,184.013L186.304,182.783ZM199.276,187.25L197.368,186.022L198.094,185.053L199.784,186.622L200.382,184.264L201.546,184.665L200.537,186.923L202.814,186.737L202.782,187.969L200.499,187.725L201.333,189.933L200.15,190.293L199.709,187.928L197.926,189.462L197.2,188.48L199.276,187.25ZM212.249,191.717L210.34,190.489L211.066,189.519L212.756,191.089L213.354,188.731L214.518,189.132L213.509,191.389L215.786,191.204L215.754,192.436L213.471,192.191L214.305,194.399L213.122,194.759L212.681,192.395L210.898,193.928L210.172,192.946L212.249,191.717ZM225.221,196.183L223.312,194.955L224.038,193.986L225.728,195.555L226.326,193.198L227.491,193.599L226.481,195.856L228.758,195.67L228.726,196.903L226.443,196.658L227.277,198.866L226.094,199.226L225.653,196.862L223.87,198.395L223.144,197.413L225.221,196.183Z"
android:fillColor="#2090EA"/>
</group>
</vector>

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"

View file

@ -0,0 +1 @@
{"v":"5.5.2","fr":60,"ip":0,"op":190,"w":100,"h":100,"nm":"Progress indicator - Indeterminate - End Fail","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check2 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":114,"s":[0]},{"t":117,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,51,0],"ix":2},"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.019,-19.126],[3.135,7.112],[-3.163,7.112],[-3.991,-19.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.293,0],[0,-2.293],[2.32,0],[0,2.292]],"o":[[2.32,0],[0,2.292],[-2.293,0],[0,-2.293]],"v":[[-0.014,11.339],[4.184,15.232],[-0.014,19.126],[-4.185,15.232]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125,0.564999988032,0.917999985639,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[50.047,48.874],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":190,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"1","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":184,"s":[100]},{"t":189,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-26]},{"t":81,"s":[0]}],"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.1254902035,0.564705908298,0.917647063732,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":96,"s":[0]},{"t":107,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[5.5]},{"t":96,"s":[100]}],"ix":2},"o":{"a":0,"k":1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":190,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Rotator","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":323,"s":[1444]}],"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":190,"st":0,"bm":0}],"markers":[]}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"v":"5.5.2","fr":60,"ip":0,"op":189,"w":100,"h":100,"nm":"Progress indicator - Indeterminate - End success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":107,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":109,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":183,"s":[100]},{"t":188,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.5,51,0],"ix":2},"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[81,81,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-19.278,2.378],[-7.103,14.354],[19.278,-14.354]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1254902035,0.564705908298,0.917647063732,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8.5,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[49.136,49.354],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":116,"s":[0]},{"t":122,"s":[100]}],"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":189,"st":-1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"1","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":183,"s":[100]},{"t":188,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-1,"s":[-26]},{"t":80,"s":[0]}],"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.1254902035,0.564705908298,0.917647063732,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":95,"s":[0]},{"t":106,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":-1,"s":[5.5]},{"t":95,"s":[100]}],"ix":2},"o":{"a":0,"k":1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":189,"st":-1,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Rotator","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-1,"s":[0]},{"t":322,"s":[1444]}],"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":189,"st":-1,"bm":0}],"markers":[]}

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