mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-30 23:36:32 +00:00
Implement new PIN UX.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user