Implement new PIN UX.
parent
109d67956f
commit
fb82420376
|
@ -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"/>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
public final class RequestCodes {
|
||||
|
||||
public static final int NOT_SET = -1;
|
||||
|
||||
private RequestCodes() { }
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package org.thoughtcrime.securesms.util.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class SlideUpWithSnackbarBehavior extends CoordinatorLayout.Behavior<View> {
|
||||
|
||||
public SlideUpWithSnackbarBehavior(@NonNull Context context, @Nullable AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent,
|
||||
@NonNull View child,
|
||||
@NonNull View dependency)
|
||||
{
|
||||
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
|
||||
child.setTranslationY(translationY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent,
|
||||
@NonNull View child,
|
||||
@NonNull View dependency)
|
||||
{
|
||||
child.setTranslationY(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent,
|
||||
@NonNull View child,
|
||||
@NonNull View dependency)
|
||||
{
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -91,6 +91,16 @@
|
|||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_requireKbsLockPin"
|
||||
app:destination="@id/kbsLockFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_wrongNumber"
|
||||
app:popUpTo="@id/enterCodeFragment"
|
||||
|
@ -134,6 +144,49 @@
|
|||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/kbsLockFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.KbsLockFragment"
|
||||
android:label="fragment_kbs_lock"
|
||||
tools:layout="@layout/kbs_lock_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_successfulRegistration"
|
||||
app:destination="@id/registrationCompletePlaceHolderFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_accountLocked"
|
||||
app:destination="@id/accountLockedFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<argument
|
||||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/accountLockedFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
|
||||
android:label="fragment_account_locked"
|
||||
tools:layout="@layout/account_locked_fragment">
|
||||
|
||||
<argument
|
||||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/captchaFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue