Implement new PIN UX.
This commit is contained in:
parent
109d67956f
commit
fb82420376
71 changed files with 3000 additions and 203 deletions
|
@ -414,11 +414,21 @@
|
|||
android:windowSoftInputMode="stateVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".lock.v2.CreateKbsPinActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".lock.v2.KbsMigrationActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".ClearProfileAvatarActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:icon="@drawable/clear_profile_avatar"
|
||||
android:label="@string/AndroidManifest_remove_photo">
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:icon="@drawable/clear_profile_avatar"
|
||||
android:label="@string/AndroidManifest_remove_photo">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"/>
|
||||
|
|
|
@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
|||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
|
@ -250,6 +251,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
|||
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.registrationValues().onNewInstall();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
}
|
||||
|
|
|
@ -14,9 +14,14 @@ import androidx.fragment.app.Fragment;
|
|||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
|
@ -35,6 +40,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
|
||||
private static final int STATE_EXPERIENCE_UPGRADE = 4;
|
||||
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
|
||||
private static final int STATE_CREATE_PROFILE_NAME = 6;
|
||||
private static final int STATE_CREATE_KBS_PIN = 7;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
|
@ -150,6 +157,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
|
||||
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
|
||||
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
|
||||
case STATE_CREATE_KBS_PIN: return getCreateKbsPinIntent();
|
||||
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
@ -165,11 +174,23 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
|
||||
return STATE_EXPERIENCE_UPGRADE;
|
||||
} else if (userMustSetKbsPin()) {
|
||||
return STATE_CREATE_KBS_PIN;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else {
|
||||
return STATE_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean userMustSetKbsPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !PinUtil.userHasPin(this);
|
||||
}
|
||||
|
||||
private boolean userMustSetProfileName() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && TextSecurePreferences.getProfileName(this) == ProfileName.EMPTY;
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
|
||||
}
|
||||
|
@ -193,6 +214,22 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
|
|||
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
|
||||
}
|
||||
|
||||
private Intent getCreateKbsPinIntent() {
|
||||
|
||||
final Intent intent;
|
||||
if (userMustSetProfileName()) {
|
||||
intent = getCreateProfileNameIntent();
|
||||
} else {
|
||||
intent = getIntent();
|
||||
}
|
||||
|
||||
return getRoutedIntent(CreateKbsPinActivity.class, intent);
|
||||
}
|
||||
|
||||
private Intent getCreateProfileNameIntent() {
|
||||
return getRoutedIntent(EditProfileActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package org.thoughtcrime.securesms.animation;
|
||||
|
||||
import android.animation.Animator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
public final class AnimationRepeatListener implements Animator.AnimatorListener {
|
||||
|
||||
private final Consumer<Animator> animationConsumer;
|
||||
|
||||
public AnimationRepeatListener(@NonNull Consumer<Animator> animationConsumer) {
|
||||
this.animationConsumer = animationConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationStart(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationEnd(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onAnimationRepeat(Animator animation) {
|
||||
this.animationConsumer.accept(animation);
|
||||
}
|
||||
}
|
|
@ -33,33 +33,6 @@ import android.net.Uri;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.MenuRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -70,6 +43,30 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.MenuRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
@ -78,7 +75,6 @@ import org.thoughtcrime.securesms.MainFragment;
|
|||
import org.thoughtcrime.securesms.MainNavigator;
|
||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.components.RatingManager;
|
||||
import org.thoughtcrime.securesms.components.SearchToolbar;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
|
||||
|
@ -94,6 +90,7 @@ import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
|
|||
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -106,6 +103,7 @@ import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
|||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone;
|
||||
|
@ -231,7 +229,8 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
|||
initializeSearchListener();
|
||||
|
||||
RatingManager.showRatingDialogIfNecessary(requireContext());
|
||||
RegistrationLockDialog.showReminderIfNecessary(requireContext());
|
||||
|
||||
RegistrationLockDialog.showReminderIfNecessary(this);
|
||||
|
||||
TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
|
||||
}
|
||||
|
@ -308,6 +307,14 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||
Snackbar.make(fab, R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
|
||||
viewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConversationClicked(@NonNull ThreadRecord threadRecord) {
|
||||
getNavigator().goToConversation(threadRecord.getRecipient().getId(),
|
||||
|
@ -350,8 +357,13 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneToastRequested(int stringRes) {
|
||||
Toast.makeText(requireContext(), stringRes, Toast.LENGTH_SHORT).show();
|
||||
public void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode) {
|
||||
startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneToastRequested(@NonNull String string) {
|
||||
Snackbar.make(fab, string, Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -472,7 +484,7 @@ public class ConversationListFragment extends MainFragment implements LoaderMana
|
|||
megaphoneContainer.setVisibility(View.GONE);
|
||||
|
||||
if (megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onVisible(megaphone, this);
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import android.app.Application;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -70,7 +70,7 @@ class ConversationListViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
void onMegaphoneSnoozed(@NonNull Megaphone snoozed) {
|
||||
megaphoneRepository.markSeen(snoozed);
|
||||
megaphoneRepository.markSeen(snoozed.getEvent());
|
||||
megaphone.postValue(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
|
@ -17,6 +19,7 @@ public final class KbsValues {
|
|||
private static final String MASTER_KEY = "kbs.registration_lock_master_key";
|
||||
private static final String TOKEN_RESPONSE = "kbs.token_response";
|
||||
private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash";
|
||||
private static final String KEYBOARD_TYPE = "kbs.keyboard_type";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
|
@ -32,6 +35,7 @@ public final class KbsValues {
|
|||
.remove(V2_LOCK_ENABLED)
|
||||
.remove(TOKEN_RESPONSE)
|
||||
.remove(LOCK_LOCAL_PIN_HASH)
|
||||
.remove(KEYBOARD_TYPE)
|
||||
.commit();
|
||||
}
|
||||
|
||||
|
@ -112,4 +116,15 @@ public final class KbsValues {
|
|||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void setKeyboardType(@NonNull KbsKeyboardType keyboardType) {
|
||||
store.beginWrite()
|
||||
.putString(KEYBOARD_TYPE, keyboardType.getCode())
|
||||
.commit();
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public @NonNull KbsKeyboardType getKeyboardType() {
|
||||
return KbsKeyboardType.fromCode(store.getString(KEYBOARD_TYPE, null));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,6 +106,10 @@ public class KeyValueDataSet implements KeyValueReader {
|
|||
}
|
||||
}
|
||||
|
||||
boolean containsKey(@NonNull String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Object> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
@ -114,10 +118,6 @@ public class KeyValueDataSet implements KeyValueReader {
|
|||
return types.get(key);
|
||||
}
|
||||
|
||||
public boolean containsKey(@NonNull String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
private <E> E readValueAsType(@NonNull String key, Class<E> type, boolean nullable) {
|
||||
Object value = values.get(key);
|
||||
if ((value == null && nullable) || (value != null && value.getClass() == type)) {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class RegistrationValues {
|
||||
|
||||
private static final String REGISTRATION_COMPLETE = "registration.complete";
|
||||
private static final String PIN_REQUIRED = "registration.pin_required";
|
||||
|
||||
private final KeyValueStore store;
|
||||
|
||||
RegistrationValues(@NonNull KeyValueStore store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public synchronized void onNewInstall() {
|
||||
store.beginWrite()
|
||||
.putBoolean(REGISTRATION_COMPLETE, false)
|
||||
.putBoolean(PIN_REQUIRED, true)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public synchronized void clearRegistrationComplete() {
|
||||
onNewInstall();
|
||||
}
|
||||
|
||||
public synchronized void setRegistrationComplete() {
|
||||
store.beginWrite()
|
||||
.putBoolean(REGISTRATION_COMPLETE, true)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public synchronized boolean isPinRequired() {
|
||||
return store.getBoolean(PIN_REQUIRED, false);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
public synchronized boolean isRegistrationComplete() {
|
||||
return store.getBoolean(REGISTRATION_COMPLETE, true);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,10 @@ public final class SignalStore {
|
|||
return new KbsValues(getStore());
|
||||
}
|
||||
|
||||
public static RegistrationValues registrationValues() {
|
||||
return new RegistrationValues(getStore());
|
||||
}
|
||||
|
||||
public static String getRemoteConfig() {
|
||||
return getStore().getString(REMOTE_CONFIG, null);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.graphics.Typeface;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
@ -26,13 +27,20 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.DialogCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
@ -43,7 +51,6 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
|||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
|
@ -55,10 +62,9 @@ public final class RegistrationLockDialog {
|
|||
|
||||
private static final String TAG = Log.tag(RegistrationLockDialog.class);
|
||||
|
||||
private static final int MIN_V2_NUMERIC_PIN_LENGTH_ENTRY = 4;
|
||||
private static final int MIN_V2_NUMERIC_PIN_LENGTH_SETTING = 4;
|
||||
public static void showReminderIfNecessary(@NonNull Fragment fragment) {
|
||||
final Context context = fragment.requireContext();
|
||||
|
||||
public static void showReminderIfNecessary(@NonNull Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
|
||||
if (!RegistrationLockReminders.needsReminder(context)) return;
|
||||
|
||||
|
@ -69,6 +75,86 @@ public final class RegistrationLockDialog {
|
|||
return;
|
||||
}
|
||||
|
||||
if (FeatureFlags.pinsForAll()) {
|
||||
showReminder(context, fragment);
|
||||
} else {
|
||||
showLegacyPinReminder(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static void showReminder(@NonNull Context context, @NonNull Fragment fragment) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark_SignalAccent : R.style.RationaleDialogLight_SignalAccent)
|
||||
.setView(R.layout.kbs_pin_reminder_view)
|
||||
.setCancelable(false)
|
||||
.setOnCancelListener(d -> RegistrationLockReminders.scheduleReminder(context, false))
|
||||
.create();
|
||||
|
||||
WindowManager windowManager = ServiceUtil.getWindowManager(context);
|
||||
Display display = windowManager.getDefaultDisplay();
|
||||
DisplayMetrics metrics = new DisplayMetrics();
|
||||
display.getMetrics(metrics);
|
||||
|
||||
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
dialog.show();
|
||||
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
TextInputLayout pinWrapper = (TextInputLayout) DialogCompat.requireViewById(dialog, R.id.pin_wrapper);
|
||||
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
|
||||
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
|
||||
View skip = DialogCompat.requireViewById(dialog, R.id.skip);
|
||||
View submit = DialogCompat.requireViewById(dialog, R.id.submit);
|
||||
|
||||
SpannableString reminderText = new SpannableString(context.getString(R.string.KbsReminderDialog__to_help_you_memorize_your_pin));
|
||||
SpannableString forgotText = new SpannableString(context.getString(R.string.KbsReminderDialog__forgot_pin));
|
||||
|
||||
pinEditText.requestFocus();
|
||||
|
||||
switch (SignalStore.kbsValues().getKeyboardType()) {
|
||||
case NUMERIC:
|
||||
pinEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
break;
|
||||
case ALPHA_NUMERIC:
|
||||
pinEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||
break;
|
||||
}
|
||||
|
||||
ClickableSpan clickableSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
|
||||
fragment.startActivityForResult(CreateKbsPinActivity.getIntentForPinUpdate(context), CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
}
|
||||
};
|
||||
|
||||
forgotText.setSpan(clickableSpan, 0, forgotText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
reminder.setText(new SpannableStringBuilder(reminderText).append(" ").append(forgotText));
|
||||
reminder.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
skip.setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, false);
|
||||
});
|
||||
|
||||
PinVerifier.Callback callback = getPinWatcherCallback(context, dialog, pinWrapper);
|
||||
PinVerifier verifier = SignalStore.kbsValues().isV2RegistrationLockEnabled()
|
||||
? new V2PinVerifier()
|
||||
: new V1PinVerifier(context);
|
||||
|
||||
submit.setOnClickListener(v -> {
|
||||
Editable pinEditable = pinEditText.getText();
|
||||
|
||||
verifier.verifyPin(pinEditable == null ? null : pinEditable.toString(), callback);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated TODO [alex]: Remove after pins for all live.
|
||||
*/
|
||||
@Deprecated
|
||||
private static void showLegacyPinReminder(@NonNull Context context) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(context, ThemeUtil.isDarkTheme(context) ? R.style.RationaleDialogDark : R.style.RationaleDialogLight)
|
||||
.setView(R.layout.registration_lock_reminder_view)
|
||||
.setCancelable(true)
|
||||
|
@ -84,8 +170,8 @@ public final class RegistrationLockDialog {
|
|||
dialog.show();
|
||||
dialog.getWindow().setLayout((int)(metrics.widthPixels * .80), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
EditText pinEditText = dialog.findViewById(R.id.pin);
|
||||
TextView reminder = dialog.findViewById(R.id.reminder);
|
||||
EditText pinEditText = (EditText) DialogCompat.requireViewById(dialog, R.id.pin);
|
||||
TextView reminder = (TextView) DialogCompat.requireViewById(dialog, R.id.reminder);
|
||||
|
||||
if (pinEditText == null) throw new AssertionError();
|
||||
if (reminder == null) throw new AssertionError();
|
||||
|
@ -136,17 +222,15 @@ public final class RegistrationLockDialog {
|
|||
|
||||
private static TextWatcher getV2PinWatcher(@NonNull Context context, AlertDialog dialog) {
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getPinBackedMasterKey();
|
||||
String localPinHash = kbsValues.getLocalPinHash();
|
||||
|
||||
if (masterKey == null) throw new AssertionError("No masterKey set at time of reminder");
|
||||
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
|
||||
|
||||
return new AfterTextChanged((Editable s) -> {
|
||||
if (s == null) return;
|
||||
String pin = s.toString();
|
||||
if (TextUtils.isEmpty(pin)) return;
|
||||
if (pin.length() < MIN_V2_NUMERIC_PIN_LENGTH_ENTRY) return;
|
||||
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
|
||||
|
||||
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
|
||||
dialog.dismiss();
|
||||
|
@ -178,9 +262,9 @@ public final class RegistrationLockDialog {
|
|||
String pinValue = pin.getText().toString().replace(" ", "");
|
||||
String repeatValue = repeat.getText().toString().replace(" ", "");
|
||||
|
||||
if (pinValue.length() < MIN_V2_NUMERIC_PIN_LENGTH_SETTING) {
|
||||
if (pinValue.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, MIN_V2_NUMERIC_PIN_LENGTH_SETTING),
|
||||
context.getString(R.string.RegistrationLockDialog_the_registration_lock_pin_must_be_at_least_d_digits, KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH),
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
@ -325,4 +409,78 @@ public final class RegistrationLockDialog {
|
|||
dialog.show();
|
||||
}
|
||||
|
||||
private static PinVerifier.Callback getPinWatcherCallback(@NonNull Context context,
|
||||
@NonNull AlertDialog dialog,
|
||||
@NonNull TextInputLayout inputWrapper)
|
||||
{
|
||||
return new PinVerifier.Callback() {
|
||||
@Override
|
||||
public void onPinCorrect() {
|
||||
dialog.dismiss();
|
||||
RegistrationLockReminders.scheduleReminder(context, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPinWrong() {
|
||||
inputWrapper.setError(context.getString(R.string.KbsReminderDialog__incorrect_pin_try_again));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final class V1PinVerifier implements PinVerifier {
|
||||
|
||||
private final String pinInPreferences;
|
||||
|
||||
private V1PinVerifier(@NonNull Context context) {
|
||||
//noinspection deprecation Acceptable to check the old pin in a reminder on a non-migrated system.
|
||||
this.pinInPreferences = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
|
||||
if (pin != null && pin.replace(" ", "").equals(pinInPreferences)) {
|
||||
callback.onPinCorrect();
|
||||
|
||||
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
|
||||
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
|
||||
} else {
|
||||
callback.onPinWrong();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class V2PinVerifier implements PinVerifier {
|
||||
|
||||
private final String localPinHash;
|
||||
|
||||
V2PinVerifier() {
|
||||
localPinHash = SignalStore.kbsValues().getLocalPinHash();
|
||||
|
||||
if (localPinHash == null) throw new AssertionError("No local pin hash set at time of reminder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyPin(@Nullable String pin, @NonNull Callback callback) {
|
||||
if (pin == null) return;
|
||||
if (TextUtils.isEmpty(pin)) return;
|
||||
|
||||
if (pin.length() < KbsConstants.MINIMUM_POSSIBLE_PIN_LENGTH) return;
|
||||
|
||||
if (PinHashing.verifyLocalPinHash(localPinHash, pin)) {
|
||||
callback.onPinCorrect();
|
||||
} else {
|
||||
callback.onPinWrong();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private interface PinVerifier {
|
||||
|
||||
void verifyPin(@Nullable String pin, @NonNull PinVerifier.Callback callback);
|
||||
|
||||
interface Callback {
|
||||
void onPinCorrect();
|
||||
void onPinWrong();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,19 +35,19 @@ public class RegistrationLockReminders {
|
|||
}
|
||||
|
||||
public static void scheduleReminder(@NonNull Context context, boolean success) {
|
||||
Long nextReminderInterval;
|
||||
|
||||
if (success) {
|
||||
long timeSinceLastReminder = System.currentTimeMillis() - TextSecurePreferences.getRegistrationLockLastReminderTime(context);
|
||||
nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
|
||||
if (nextReminderInterval == null) nextReminderInterval = INTERVALS.last();
|
||||
} else {
|
||||
long lastReminderInterval = TextSecurePreferences.getRegistrationLockNextReminderInterval(context);
|
||||
nextReminderInterval = INTERVALS.lower(lastReminderInterval);
|
||||
if (nextReminderInterval == null) nextReminderInterval = INTERVALS.first();
|
||||
}
|
||||
Long nextReminderInterval = INTERVALS.higher(timeSinceLastReminder);
|
||||
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
|
||||
if (nextReminderInterval == null) {
|
||||
nextReminderInterval = INTERVALS.last();
|
||||
}
|
||||
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, nextReminderInterval);
|
||||
} else {
|
||||
long timeSinceLastReminder = TextSecurePreferences.getRegistrationLockLastReminderTime(context) + TimeUnit.MINUTES.toMillis(5);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, timeSinceLastReminder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
abstract class BaseKbsPinFragment<ViewModel extends BaseKbsPinViewModel> extends Fragment {
|
||||
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private EditText input;
|
||||
private TextView label;
|
||||
private TextView keyboardToggle;
|
||||
private TextView confirm;
|
||||
private LottieAnimationView lottieProgress;
|
||||
private LottieAnimationView lottieEnd;
|
||||
private ViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.base_kbs_pin_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeViews(view);
|
||||
|
||||
viewModel = initializeViewModel();
|
||||
viewModel.getUserEntry().observe(getViewLifecycleOwner(), kbsPin -> {
|
||||
boolean isEntryValid = kbsPin.length() >= KbsConstants.MINIMUM_NEW_PIN_LENGTH;
|
||||
|
||||
confirm.setEnabled(isEntryValid);
|
||||
confirm.setAlpha(isEntryValid ? 1f : 0.5f);
|
||||
});
|
||||
|
||||
viewModel.getKeyboard().observe(getViewLifecycleOwner(), keyboardType -> {
|
||||
updateKeyboard(keyboardType);
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
initializeListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
input.requestFocus();
|
||||
}
|
||||
|
||||
protected abstract ViewModel initializeViewModel();
|
||||
|
||||
protected abstract void initializeViewStates();
|
||||
|
||||
protected TextView getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
protected TextView getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
protected EditText getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
protected LottieAnimationView getLottieProgress() {
|
||||
return lottieProgress;
|
||||
}
|
||||
|
||||
protected LottieAnimationView getLottieEnd() {
|
||||
return lottieEnd;
|
||||
}
|
||||
|
||||
protected TextView getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
protected TextView getKeyboardToggle() {
|
||||
return keyboardToggle;
|
||||
}
|
||||
|
||||
protected TextView getConfirm() {
|
||||
return confirm;
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull View view) {
|
||||
title = view.findViewById(R.id.edit_kbs_pin_title);
|
||||
description = view.findViewById(R.id.edit_kbs_pin_description);
|
||||
input = view.findViewById(R.id.edit_kbs_pin_input);
|
||||
label = view.findViewById(R.id.edit_kbs_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.edit_kbs_pin_keyboard_toggle);
|
||||
confirm = view.findViewById(R.id.edit_kbs_pin_confirm);
|
||||
lottieProgress = view.findViewById(R.id.edit_kbs_pin_lottie_progress);
|
||||
lottieEnd = view.findViewById(R.id.edit_kbs_pin_lottie_end);
|
||||
|
||||
initializeViewStates();
|
||||
}
|
||||
|
||||
private void initializeListeners() {
|
||||
input.addTextChangedListener(new AfterTextChanged(s -> viewModel.setUserEntry(s.toString())));
|
||||
input.setImeOptions(EditorInfo.IME_ACTION_NEXT);
|
||||
input.setOnEditorActionListener(this::handleEditorAction);
|
||||
keyboardToggle.setOnClickListener(v -> viewModel.toggleAlphaNumeric());
|
||||
confirm.setOnClickListener(v -> viewModel.confirm());
|
||||
}
|
||||
|
||||
private boolean handleEditorAction(@NonNull View view, int actionId, @NonNull KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT && confirm.isEnabled()) {
|
||||
viewModel.confirm();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateKeyboard(@NonNull KbsKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == KbsKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
input.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
}
|
||||
|
||||
private @StringRes int resolveKeyboardToggleText(@NonNull KbsKeyboardType keyboard) {
|
||||
if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.BaseKbsPinFragment__create_numeric_pin;
|
||||
} else {
|
||||
return R.string.BaseKbsPinFragment__create_alphanumeric_pin;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
interface BaseKbsPinViewModel {
|
||||
LiveData<KbsPin> getUserEntry();
|
||||
|
||||
LiveData<KbsKeyboardType> getKeyboard();
|
||||
|
||||
@MainThread
|
||||
void setUserEntry(String userEntry);
|
||||
|
||||
@MainThread
|
||||
void toggleAlphaNumeric();
|
||||
|
||||
@MainThread
|
||||
void confirm();
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RawRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
import com.airbnb.lottie.LottieDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.animation.AnimationRepeatListener;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
|
||||
public class ConfirmKbsPinFragment extends BaseKbsPinFragment<ConfirmKbsPinViewModel> {
|
||||
|
||||
private ConfirmKbsPinFragmentArgs args;
|
||||
private ConfirmKbsPinViewModel viewModel;
|
||||
|
||||
@Override
|
||||
protected void initializeViewStates() {
|
||||
args = ConfirmKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
if (args.getIsNewPin()) {
|
||||
initializeViewStatesForNewPin();
|
||||
} else {
|
||||
initializeViewStatesForPin();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfirmKbsPinViewModel initializeViewModel() {
|
||||
KbsPin userEntry = Preconditions.checkNotNull(args.getUserEntry());
|
||||
KbsKeyboardType keyboard = args.getKeyboard();
|
||||
ConfirmKbsPinRepository repository = new ConfirmKbsPinRepository();
|
||||
ConfirmKbsPinViewModel.Factory factory = new ConfirmKbsPinViewModel.Factory(userEntry, keyboard, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ConfirmKbsPinViewModel.class);
|
||||
|
||||
viewModel.getLabel().observe(getViewLifecycleOwner(), this::updateLabel);
|
||||
viewModel.getSaveAnimation().observe(getViewLifecycleOwner(), this::updateSaveAnimation);
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private void initializeViewStatesForNewPin() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
|
||||
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
|
||||
getKeyboardToggle().setVisibility(View.INVISIBLE);
|
||||
getLabel().setText("");
|
||||
}
|
||||
|
||||
private void initializeViewStatesForPin() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
|
||||
getDescription().setText(R.string.ConfirmKbsPinFragment__confirm_your_pin);
|
||||
getKeyboardToggle().setVisibility(View.INVISIBLE);
|
||||
getLabel().setText("");
|
||||
}
|
||||
|
||||
private void updateLabel(@NonNull ConfirmKbsPinViewModel.Label label) {
|
||||
switch (label) {
|
||||
case EMPTY:
|
||||
getLabel().setText("");
|
||||
break;
|
||||
case CREATING_PIN:
|
||||
getLabel().setText(R.string.ConfirmKbsPinFragment__creating_pin);
|
||||
break;
|
||||
case RE_ENTER_PIN:
|
||||
getLabel().setText(R.string.ConfirmKbsPinFragment__re_enter_pin);
|
||||
break;
|
||||
case PIN_DOES_NOT_MATCH:
|
||||
getLabel().setText(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.red),
|
||||
getString(R.string.ConfirmKbsPinFragment__pins_dont_match)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSaveAnimation(@NonNull ConfirmKbsPinViewModel.SaveAnimation animation) {
|
||||
updateAnimationAndInputVisibility(animation);
|
||||
LottieAnimationView lottieProgress = getLottieProgress();
|
||||
|
||||
switch (animation) {
|
||||
case NONE:
|
||||
lottieProgress.cancelAnimation();
|
||||
break;
|
||||
case LOADING:
|
||||
lottieProgress.setAnimation(R.raw.lottie_kbs_loading);
|
||||
lottieProgress.setRepeatMode(LottieDrawable.RESTART);
|
||||
lottieProgress.setRepeatCount(LottieDrawable.INFINITE);
|
||||
lottieProgress.playAnimation();
|
||||
break;
|
||||
case SUCCESS:
|
||||
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_success, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
requireActivity().setResult(Activity.RESULT_OK);
|
||||
closeNavGraphBranch();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case FAILURE:
|
||||
startEndAnimationOnNextProgressRepetition(R.raw.lottie_kbs_failure, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
displayFailedDialog();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void startEndAnimationOnNextProgressRepetition(@RawRes int lottieAnimationId,
|
||||
@NonNull AnimationCompleteListener listener)
|
||||
{
|
||||
LottieAnimationView lottieProgress = getLottieProgress();
|
||||
LottieAnimationView lottieEnd = getLottieEnd();
|
||||
|
||||
lottieEnd.setAnimation(lottieAnimationId);
|
||||
lottieEnd.removeAllAnimatorListeners();
|
||||
lottieEnd.setRepeatCount(0);
|
||||
lottieEnd.addAnimatorListener(listener);
|
||||
|
||||
if (lottieProgress.isAnimating()) {
|
||||
lottieProgress.addAnimatorListener(new AnimationRepeatListener(animator ->
|
||||
hideProgressAndStartEndAnimation(lottieProgress, lottieEnd)
|
||||
));
|
||||
} else {
|
||||
hideProgressAndStartEndAnimation(lottieProgress, lottieEnd);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideProgressAndStartEndAnimation(@NonNull LottieAnimationView lottieProgress,
|
||||
@NonNull LottieAnimationView lottieEnd)
|
||||
{
|
||||
viewModel.onLoadingAnimationComplete();
|
||||
lottieProgress.setVisibility(View.GONE);
|
||||
lottieEnd.setVisibility(View.VISIBLE);
|
||||
lottieEnd.playAnimation();
|
||||
}
|
||||
|
||||
private void updateAnimationAndInputVisibility(ConfirmKbsPinViewModel.SaveAnimation saveAnimation) {
|
||||
if (saveAnimation == ConfirmKbsPinViewModel.SaveAnimation.NONE) {
|
||||
getInput().setVisibility(View.VISIBLE);
|
||||
getLottieProgress().setVisibility(View.GONE);
|
||||
} else {
|
||||
getInput().setVisibility(View.GONE);
|
||||
getLottieProgress().setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayFailedDialog() {
|
||||
new AlertDialog.Builder(requireContext()).setTitle(R.string.ConfirmKbsPinFragment__pin_creation_failed)
|
||||
.setMessage(R.string.ConfirmKbsPinFragment__your_pin_was_not_saved)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, (d, w) -> {
|
||||
d.dismiss();
|
||||
markMegaphoneSeenIfNecessary();
|
||||
requireActivity().setResult(Activity.RESULT_CANCELED);
|
||||
closeNavGraphBranch();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void closeNavGraphBranch() {
|
||||
Intent activityIntent = requireActivity().getIntent();
|
||||
if (activityIntent != null && activityIntent.hasExtra("next_intent")) {
|
||||
startActivity(activityIntent.getParcelableExtra("next_intent"));
|
||||
}
|
||||
|
||||
requireActivity().finish();
|
||||
}
|
||||
|
||||
private void markMegaphoneSeenIfNecessary() {
|
||||
ApplicationDependencies.getMegaphoneRepository().markSeen(Megaphones.Event.PINS_FOR_ALL);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.PinHashing;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
|
||||
import org.whispersystems.signalservice.api.RegistrationLockData;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
final class ConfirmKbsPinRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ConfirmKbsPinRepository.class);
|
||||
|
||||
void setPin(@NonNull KbsPin kbsPin, @NonNull KbsKeyboardType keyboard, @NonNull Consumer<PinSetResult> resultConsumer) {
|
||||
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
String pinValue = kbsPin.toString();
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
try {
|
||||
Log.i(TAG, "Setting pin on KBS");
|
||||
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
|
||||
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
|
||||
HashedPin hashedPin = PinHashing.hashPin(pinValue, pinChangeSession);
|
||||
RegistrationLockData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
|
||||
RegistrationLockData restoredData = keyBackupService.newRestoreSession(kbsData.getTokenResponse())
|
||||
.restorePin(hashedPin);
|
||||
|
||||
if (!restoredData.getMasterKey().equals(masterKey)) {
|
||||
throw new AssertionError("Failed to set the pin correctly");
|
||||
} else {
|
||||
Log.i(TAG, "Set and retrieved pin on KBS successfully");
|
||||
}
|
||||
|
||||
kbsValues.setRegistrationLockMasterKey(restoredData, PinHashing.localPinHash(pinValue));
|
||||
TextSecurePreferences.clearOldRegistrationLockPin(context);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
||||
SignalStore.kbsValues().setKeyboardType(keyboard);
|
||||
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL);
|
||||
|
||||
return PinSetResult.SUCCESS;
|
||||
} catch (IOException | UnauthenticatedResponseException | KeyBackupServicePinException e) {
|
||||
Log.w(TAG, e);
|
||||
return PinSetResult.FAILURE;
|
||||
}
|
||||
}, resultConsumer::accept);
|
||||
}
|
||||
|
||||
enum PinSetResult {
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinRepository.PinSetResult;
|
||||
|
||||
final class ConfirmKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
|
||||
|
||||
private final ConfirmKbsPinRepository repository;
|
||||
|
||||
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
|
||||
private final MutableLiveData<KbsKeyboardType> keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC);
|
||||
private final MutableLiveData<SaveAnimation> saveAnimation = new MutableLiveData<>(SaveAnimation.NONE);
|
||||
private final MutableLiveData<Label> label = new MutableLiveData<>(Label.RE_ENTER_PIN);
|
||||
|
||||
private final KbsPin pinToConfirm;
|
||||
|
||||
private ConfirmKbsPinViewModel(@NonNull KbsPin pinToConfirm,
|
||||
@NonNull KbsKeyboardType keyboard,
|
||||
@NonNull ConfirmKbsPinRepository repository)
|
||||
{
|
||||
this.keyboard.setValue(keyboard);
|
||||
|
||||
this.pinToConfirm = pinToConfirm;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
LiveData<SaveAnimation> getSaveAnimation() {
|
||||
return Transformations.distinctUntilChanged(saveAnimation);
|
||||
}
|
||||
|
||||
LiveData<Label> getLabel() {
|
||||
return Transformations.distinctUntilChanged(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirm() {
|
||||
KbsPin userEntry = this.userEntry.getValue();
|
||||
|
||||
if (pinToConfirm.toString().equals(userEntry.toString())) {
|
||||
this.label.setValue(Label.CREATING_PIN);
|
||||
this.userEntry.setValue(KbsPin.EMPTY);
|
||||
this.saveAnimation.setValue(SaveAnimation.LOADING);
|
||||
|
||||
repository.setPin(pinToConfirm, Preconditions.checkNotNull(this.keyboard.getValue()), this::handleResult);
|
||||
} else {
|
||||
this.label.setValue(Label.PIN_DOES_NOT_MATCH);
|
||||
}
|
||||
}
|
||||
|
||||
void onLoadingAnimationComplete() {
|
||||
this.label.setValue(Label.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<KbsPin> getUserEntry() {
|
||||
return userEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<KbsKeyboardType> getKeyboard() {
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setUserEntry(String userEntry) {
|
||||
this.userEntry.setValue(KbsPin.from(userEntry));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void toggleAlphaNumeric() {
|
||||
this.keyboard.setValue(this.keyboard.getValue().getOther());
|
||||
}
|
||||
|
||||
private void handleResult(PinSetResult result) {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
this.saveAnimation.setValue(SaveAnimation.SUCCESS);
|
||||
break;
|
||||
case FAILURE:
|
||||
this.saveAnimation.setValue(SaveAnimation.FAILURE);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown state: " + result.name());
|
||||
}
|
||||
}
|
||||
|
||||
enum Label {
|
||||
RE_ENTER_PIN,
|
||||
PIN_DOES_NOT_MATCH,
|
||||
CREATING_PIN,
|
||||
EMPTY
|
||||
}
|
||||
|
||||
enum SaveAnimation {
|
||||
NONE,
|
||||
LOADING,
|
||||
SUCCESS,
|
||||
FAILURE
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final KbsPin pinToConfirm;
|
||||
private final KbsKeyboardType keyboard;
|
||||
private final ConfirmKbsPinRepository repository;
|
||||
|
||||
Factory(@NonNull KbsPin pinToConfirm,
|
||||
@NonNull KbsKeyboardType keyboard,
|
||||
@NonNull ConfirmKbsPinRepository repository)
|
||||
{
|
||||
this.pinToConfirm = pinToConfirm;
|
||||
this.keyboard = keyboard;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new ConfirmKbsPinViewModel(pinToConfirm, keyboard, repository);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.navigation.NavGraph;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphrasePromptActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class CreateKbsPinActivity extends BaseActionBarActivity {
|
||||
|
||||
public static final int REQUEST_NEW_PIN = 27698;
|
||||
|
||||
private static final String IS_NEW_PIN = "is_new_pin";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
|
||||
public static Intent getIntentForPinCreate(@NonNull Context context) {
|
||||
return new Intent(context, CreateKbsPinActivity.class);
|
||||
}
|
||||
|
||||
public static Intent getIntentForPinUpdate(@NonNull Context context) {
|
||||
Intent intent = getIntentForPinCreate(context);
|
||||
|
||||
intent.putExtra(IS_NEW_PIN, true);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
if (KeyCachingService.isLocked(this)) {
|
||||
startActivity(getPromptPassphraseIntent());
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.create_kbs_pin_activity);
|
||||
|
||||
CreateKbsPinFragmentArgs arguments = CreateKbsPinFragmentArgs.fromBundle(getIntent().getExtras());
|
||||
|
||||
NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph();
|
||||
Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
return intent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class CreateKbsPinFragment extends BaseKbsPinFragment<CreateKbsPinViewModel> {
|
||||
|
||||
private CreateKbsPinFragmentArgs args;
|
||||
|
||||
@Override
|
||||
protected void initializeViewStates() {
|
||||
args = CreateKbsPinFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
if (args.getIsNewPin()) {
|
||||
initializeViewStatesForNewPin();
|
||||
} else {
|
||||
initializeViewStatesForPin();
|
||||
}
|
||||
|
||||
getLabel().setText(getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits));
|
||||
getConfirm().setEnabled(false);
|
||||
}
|
||||
|
||||
private void initializeViewStatesForPin() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_your_pin);
|
||||
getDescription().setText(R.string.CreateKbsPinFragment__pins_add_an_extra_layer_of_security);
|
||||
|
||||
}
|
||||
|
||||
private void initializeViewStatesForNewPin() {
|
||||
getTitle().setText(R.string.CreateKbsPinFragment__create_a_new_pin);
|
||||
getDescription().setText(R.string.CreateKbsPinFragment__because_youre_still_logged_in);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CreateKbsPinViewModel initializeViewModel() {
|
||||
CreateKbsPinViewModel viewModel = ViewModelProviders.of(this).get(CreateKbsPinViewModel.class);
|
||||
|
||||
viewModel.getKeyboard().observe(getViewLifecycleOwner(), k -> getLabel().setText(getLabelText(k)));
|
||||
viewModel.getNavigationEvents().observe(getViewLifecycleOwner(), e -> onConfirmPin(e.getUserEntry(), e.getKeyboard()));
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
private void onConfirmPin(@NonNull KbsPin userEntry, @NonNull KbsKeyboardType keyboard) {
|
||||
CreateKbsPinFragmentDirections.ActionConfirmPin action = CreateKbsPinFragmentDirections.actionConfirmPin();
|
||||
|
||||
action.setUserEntry(userEntry);
|
||||
action.setKeyboard(keyboard);
|
||||
action.setIsNewPin(args.getIsNewPin());
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(action);
|
||||
}
|
||||
|
||||
private String getLabelText(@NonNull KbsKeyboardType keyboard) {
|
||||
if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) {
|
||||
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_characters);
|
||||
} else {
|
||||
return getPinLengthRestrictionText(R.plurals.CreateKbsPinFragment__pin_must_be_at_least_digits);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPinLengthRestrictionText(@PluralsRes int plurals) {
|
||||
return requireContext().getResources().getQuantityString(plurals, KbsConstants.MINIMUM_NEW_PIN_LENGTH, KbsConstants.MINIMUM_NEW_PIN_LENGTH);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Preconditions;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
|
||||
public final class CreateKbsPinViewModel extends ViewModel implements BaseKbsPinViewModel {
|
||||
|
||||
private final MutableLiveData<KbsPin> userEntry = new MutableLiveData<>(KbsPin.EMPTY);
|
||||
private final MutableLiveData<KbsKeyboardType> keyboard = new MutableLiveData<>(KbsKeyboardType.NUMERIC);
|
||||
private final SingleLiveEvent<NavigationEvent> events = new SingleLiveEvent<>();
|
||||
|
||||
@Override
|
||||
public LiveData<KbsPin> getUserEntry() {
|
||||
return userEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<KbsKeyboardType> getKeyboard() {
|
||||
return keyboard;
|
||||
}
|
||||
|
||||
LiveData<NavigationEvent> getNavigationEvents() { return events; }
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void setUserEntry(String userEntry) {
|
||||
this.userEntry.setValue(KbsPin.from(userEntry));
|
||||
}
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void toggleAlphaNumeric() {
|
||||
this.keyboard.setValue(Preconditions.checkNotNull(this.keyboard.getValue()).getOther());
|
||||
}
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void confirm() {
|
||||
events.setValue(new NavigationEvent(Preconditions.checkNotNull(this.getUserEntry().getValue()),
|
||||
Preconditions.checkNotNull(this.getKeyboard().getValue())));
|
||||
}
|
||||
|
||||
static final class NavigationEvent {
|
||||
private final KbsPin userEntry;
|
||||
private final KbsKeyboardType keyboard;
|
||||
|
||||
NavigationEvent(@NonNull KbsPin userEntry, @NonNull KbsKeyboardType keyboard) {
|
||||
this.userEntry = userEntry;
|
||||
this.keyboard = keyboard;
|
||||
}
|
||||
|
||||
KbsPin getUserEntry() {
|
||||
return userEntry;
|
||||
}
|
||||
|
||||
KbsKeyboardType getKeyboard() {
|
||||
return keyboard;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
public final class KbsConstants {
|
||||
|
||||
static final int MINIMUM_NEW_PIN_LENGTH = 6;
|
||||
|
||||
/** Migrated pins from V1 might be 4 */
|
||||
public static final int MINIMUM_POSSIBLE_PIN_LENGTH = 4;
|
||||
|
||||
private KbsConstants() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum KbsKeyboardType {
|
||||
NUMERIC("numeric"),
|
||||
ALPHA_NUMERIC("alphaNumeric");
|
||||
|
||||
private final String code;
|
||||
|
||||
KbsKeyboardType(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
KbsKeyboardType getOther() {
|
||||
if (this == NUMERIC) return ALPHA_NUMERIC;
|
||||
else return NUMERIC;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static KbsKeyboardType fromCode(@Nullable String code) {
|
||||
for (KbsKeyboardType type : KbsKeyboardType.values()) {
|
||||
if (type.code.equals(code)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return NUMERIC;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PassphrasePromptActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.DynamicRegistrationTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
|
||||
public class KbsMigrationActivity extends BaseActionBarActivity {
|
||||
|
||||
public static final int REQUEST_NEW_PIN = CreateKbsPinActivity.REQUEST_NEW_PIN;
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicRegistrationTheme();
|
||||
|
||||
public static Intent createIntent() {
|
||||
return new Intent(ApplicationDependencies.getApplication(), KbsMigrationActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
if (KeyCachingService.isLocked(this)) {
|
||||
startActivity(getPromptPassphraseIntent());
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.kbs_migration_activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
return getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
}
|
||||
|
||||
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
|
||||
final Intent intent = new Intent(this, destination);
|
||||
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
|
||||
return intent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class KbsPin implements Parcelable {
|
||||
|
||||
public static KbsPin EMPTY = new KbsPin("");
|
||||
|
||||
private final String pin;
|
||||
|
||||
private KbsPin(String pin) {
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
private KbsPin(Parcel in) {
|
||||
pin = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return pin;
|
||||
}
|
||||
|
||||
public static KbsPin from(@Nullable String pin) {
|
||||
if (pin == null) return EMPTY;
|
||||
|
||||
pin = pin.trim();
|
||||
|
||||
if (pin.length() == 0) return EMPTY;
|
||||
|
||||
return new KbsPin(pin);
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return pin.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(pin);
|
||||
}
|
||||
|
||||
public static final Creator<KbsPin> CREATOR = new Creator<KbsPin>() {
|
||||
@Override
|
||||
public KbsPin createFromParcel(Parcel in) {
|
||||
return new KbsPin(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KbsPin[] newArray(int size) {
|
||||
return new KbsPin[size];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public final class KbsSplashFragment extends Fragment {
|
||||
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private TextView primaryAction;
|
||||
private TextView secondaryAction;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.kbs_splash_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
title = view.findViewById(R.id.kbs_splash_title);
|
||||
description = view.findViewById(R.id.kbs_splash_description);
|
||||
primaryAction = view.findViewById(R.id.kbs_splash_primary_action);
|
||||
secondaryAction = view.findViewById(R.id.kbs_splash_secondary_action);
|
||||
|
||||
primaryAction.setOnClickListener(v -> onCreatePin());
|
||||
secondaryAction.setOnClickListener(v -> onLearnMore());
|
||||
|
||||
if (TextSecurePreferences.isV1RegistrationLockEnabled(requireContext())) {
|
||||
setUpRegLockEnabled();
|
||||
} else {
|
||||
setUpRegLockDisabled();
|
||||
}
|
||||
|
||||
description.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() { }
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpRegLockEnabled() {
|
||||
title.setText(R.string.KbsSplashFragment__registration_lock_equals_pin);
|
||||
description.setText(R.string.KbsSplashFragment__your_registration_lock_is_now_called_a_pin);
|
||||
primaryAction.setText(R.string.KbsSplashFragment__update_pin);
|
||||
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
|
||||
}
|
||||
|
||||
private void setUpRegLockDisabled() {
|
||||
title.setText(R.string.KbsSplashFragment__introducing_pins);
|
||||
description.setText(R.string.KbsSplashFragment__pins_add_another_level_of_security_to_your_account);
|
||||
primaryAction.setText(R.string.KbsSplashFragment__create_your_pin);
|
||||
secondaryAction.setText(R.string.KbsSplashFragment__learn_more);
|
||||
}
|
||||
|
||||
private void onCreatePin() {
|
||||
Navigation.findNavController(requireView()).navigate(KbsSplashFragmentDirections.actionCreateKbsPin());
|
||||
}
|
||||
|
||||
private void onLearnMore() {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
||||
intent.setData(Uri.parse(getString(R.string.KbsSplashFragment__learn_more_link)));
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.thoughtcrime.securesms.lock.v2;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public final class PinUtil {
|
||||
|
||||
private PinUtil() {}
|
||||
|
||||
public static boolean userHasPin(@NonNull Context context) {
|
||||
return TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ public class BasicMegaphoneView extends FrameLayout {
|
|||
super.onAttachedToWindow();
|
||||
|
||||
if (megaphone != null && megaphoneListener != null && megaphone.getOnVisibleListener() != null) {
|
||||
megaphone.getOnVisibleListener().onVisible(megaphone, megaphoneListener);
|
||||
megaphone.getOnVisibleListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ public class BasicMegaphoneView extends FrameLayout {
|
|||
actionButton.setText(megaphone.getButtonText());
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (megaphone.getButtonClickListener() != null) {
|
||||
megaphone.getButtonClickListener().onClick(megaphone, megaphoneListener);
|
||||
megaphone.getButtonClickListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -91,7 +91,13 @@ public class BasicMegaphoneView extends FrameLayout {
|
|||
|
||||
if (megaphone.canSnooze()) {
|
||||
snoozeButton.setVisibility(VISIBLE);
|
||||
snoozeButton.setOnClickListener(v -> megaphoneListener.onMegaphoneSnooze(megaphone));
|
||||
snoozeButton.setOnClickListener(v -> {
|
||||
megaphoneListener.onMegaphoneSnooze(megaphone);
|
||||
|
||||
if (megaphone.getSnoozeListener() != null) {
|
||||
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
}
|
||||
|
|
|
@ -12,32 +12,29 @@ import org.thoughtcrime.securesms.megaphone.Megaphones.Event;
|
|||
*/
|
||||
public class Megaphone {
|
||||
|
||||
/** For {@link #getMaxAppearances()}. */
|
||||
public static final int UNLIMITED = -1;
|
||||
|
||||
private final Event event;
|
||||
private final Style style;
|
||||
private final boolean mandatory;
|
||||
private final boolean canSnooze;
|
||||
private final int maxAppearances;
|
||||
private final int titleRes;
|
||||
private final int bodyRes;
|
||||
private final int imageRes;
|
||||
private final int buttonTextRes;
|
||||
private final OnClickListener buttonListener;
|
||||
private final OnVisibleListener onVisibleListener;
|
||||
private final Event event;
|
||||
private final Style style;
|
||||
private final boolean mandatory;
|
||||
private final boolean canSnooze;
|
||||
private final int titleRes;
|
||||
private final int bodyRes;
|
||||
private final int imageRes;
|
||||
private final int buttonTextRes;
|
||||
private final EventListener buttonListener;
|
||||
private final EventListener snoozeListener;
|
||||
private final EventListener onVisibleListener;
|
||||
|
||||
private Megaphone(@NonNull Builder builder) {
|
||||
this.event = builder.event;
|
||||
this.style = builder.style;
|
||||
this.mandatory = builder.mandatory;
|
||||
this.canSnooze = builder.canSnooze;
|
||||
this.maxAppearances = builder.maxAppearances;
|
||||
this.titleRes = builder.titleRes;
|
||||
this.bodyRes = builder.bodyRes;
|
||||
this.imageRes = builder.imageRes;
|
||||
this.buttonTextRes = builder.buttonTextRes;
|
||||
this.buttonListener = builder.buttonListener;
|
||||
this.snoozeListener = builder.snoozeListener;
|
||||
this.onVisibleListener = builder.onVisibleListener;
|
||||
}
|
||||
|
||||
|
@ -49,10 +46,6 @@ public class Megaphone {
|
|||
return mandatory;
|
||||
}
|
||||
|
||||
public int getMaxAppearances() {
|
||||
return maxAppearances;
|
||||
}
|
||||
|
||||
public boolean canSnooze() {
|
||||
return canSnooze;
|
||||
}
|
||||
|
@ -77,11 +70,15 @@ public class Megaphone {
|
|||
return buttonTextRes;
|
||||
}
|
||||
|
||||
public @Nullable OnClickListener getButtonClickListener() {
|
||||
public @Nullable EventListener getButtonClickListener() {
|
||||
return buttonListener;
|
||||
}
|
||||
|
||||
public @Nullable OnVisibleListener getOnVisibleListener() {
|
||||
public @Nullable EventListener getSnoozeListener() {
|
||||
return buttonListener;
|
||||
}
|
||||
|
||||
public @Nullable EventListener getOnVisibleListener() {
|
||||
return onVisibleListener;
|
||||
}
|
||||
|
||||
|
@ -90,21 +87,20 @@ public class Megaphone {
|
|||
private final Event event;
|
||||
private final Style style;
|
||||
|
||||
private boolean mandatory;
|
||||
private boolean canSnooze;
|
||||
private int maxAppearances;
|
||||
private int titleRes;
|
||||
private int bodyRes;
|
||||
private int imageRes;
|
||||
private int buttonTextRes;
|
||||
private OnClickListener buttonListener;
|
||||
private OnVisibleListener onVisibleListener;
|
||||
private boolean mandatory;
|
||||
private boolean canSnooze;
|
||||
private int titleRes;
|
||||
private int bodyRes;
|
||||
private int imageRes;
|
||||
private int buttonTextRes;
|
||||
private EventListener buttonListener;
|
||||
private EventListener snoozeListener;
|
||||
private EventListener onVisibleListener;
|
||||
|
||||
|
||||
public Builder(@NonNull Event event, @NonNull Style style) {
|
||||
this.event = event;
|
||||
this.style = style;
|
||||
this.maxAppearances = 1;
|
||||
}
|
||||
|
||||
public @NonNull Builder setMandatory(boolean mandatory) {
|
||||
|
@ -112,13 +108,15 @@ public class Megaphone {
|
|||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setSnooze(boolean canSnooze) {
|
||||
this.canSnooze = canSnooze;
|
||||
public @NonNull Builder enableSnooze(@Nullable EventListener listener) {
|
||||
this.canSnooze = true;
|
||||
this.snoozeListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setMaxAppearances(int maxAppearances) {
|
||||
this.maxAppearances = maxAppearances;
|
||||
public @NonNull Builder disableSnooze() {
|
||||
this.canSnooze = false;
|
||||
this.snoozeListener = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -137,13 +135,13 @@ public class Megaphone {
|
|||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull OnClickListener listener) {
|
||||
public @NonNull Builder setButtonText(@StringRes int buttonTextRes, @NonNull EventListener listener) {
|
||||
this.buttonTextRes = buttonTextRes;
|
||||
this.buttonListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setOnVisibleListener(@Nullable OnVisibleListener listener) {
|
||||
public @NonNull Builder setOnVisibleListener(@Nullable EventListener listener) {
|
||||
this.onVisibleListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
@ -157,11 +155,7 @@ public class Megaphone {
|
|||
REACTIONS, BASIC, FULLSCREEN
|
||||
}
|
||||
|
||||
public interface OnVisibleListener {
|
||||
void onVisible(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
|
||||
}
|
||||
|
||||
public interface OnClickListener {
|
||||
void onClick(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
|
||||
public interface EventListener {
|
||||
void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneListener listener);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,15 @@ public interface MegaphoneListener {
|
|||
*/
|
||||
void onMegaphoneNavigationRequested(@NonNull Intent intent);
|
||||
|
||||
/**
|
||||
* When a megaphone wants to navigate to a specific intent for a request code.
|
||||
*/
|
||||
void onMegaphoneNavigationRequested(@NonNull Intent intent, int requestCode);
|
||||
|
||||
/**
|
||||
* When a megaphone wants to show a toast/snackbar.
|
||||
*/
|
||||
void onMegaphoneToastRequested(@StringRes int stringRes);
|
||||
void onMegaphoneToastRequested(@NonNull String string);
|
||||
|
||||
/**
|
||||
* When a megaphone has been snoozed via "remind me later" or a similar option.
|
||||
|
|
|
@ -82,21 +82,12 @@ public class MegaphoneRepository {
|
|||
}
|
||||
|
||||
@MainThread
|
||||
public void markSeen(@NonNull Megaphone megaphone) {
|
||||
public void markSeen(@NonNull Event event) {
|
||||
long lastSeen = System.currentTimeMillis();
|
||||
|
||||
executor.execute(() -> {
|
||||
Event event = megaphone.getEvent();
|
||||
MegaphoneRecord record = getRecord(event);
|
||||
|
||||
if (megaphone.getMaxAppearances() != Megaphone.UNLIMITED &&
|
||||
record.getSeenCount() + 1 >= megaphone.getMaxAppearances())
|
||||
{
|
||||
database.markFinished(event);
|
||||
} else {
|
||||
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
|
||||
}
|
||||
|
||||
database.markSeen(event, record.getSeenCount() + 1, lastSeen);
|
||||
enabled = false;
|
||||
resetDatabaseCache();
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.megaphone;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -11,26 +10,29 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Creating a new megaphone:
|
||||
* - Add an enum to {@link Event}
|
||||
* - Return a megaphone in {@link #forRecord(MegaphoneRecord)}
|
||||
* - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)}
|
||||
* - Include the event in {@link #buildDisplayOrder()}
|
||||
*
|
||||
* Common patterns:
|
||||
* - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}.
|
||||
* - For events guarded by feature flags, set a {@link ForeverSchedule} with false in
|
||||
* {@link #buildDisplayOrder()}.
|
||||
* - For events that change, return different megaphones in {@link #forRecord(MegaphoneRecord)}
|
||||
* - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)}
|
||||
* based on whatever properties you're interested in.
|
||||
*/
|
||||
public final class Megaphones {
|
||||
|
@ -49,7 +51,7 @@ public final class Megaphones {
|
|||
})
|
||||
.map(Map.Entry::getKey)
|
||||
.map(records::get)
|
||||
.map(Megaphones::forRecord)
|
||||
.map(record -> Megaphones.forRecord(context, record))
|
||||
.toList();
|
||||
|
||||
boolean hasOptional = Stream.of(megaphones).anyMatch(m -> !m.isMandatory());
|
||||
|
@ -73,13 +75,16 @@ public final class Megaphones {
|
|||
private static Map<Event, MegaphoneSchedule> buildDisplayOrder() {
|
||||
return new LinkedHashMap<Event, MegaphoneSchedule>() {{
|
||||
put(Event.REACTIONS, new ForeverSchedule(FeatureFlags.reactionSending()));
|
||||
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
|
||||
}};
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone forRecord(@NonNull MegaphoneRecord record) {
|
||||
private static @NonNull Megaphone forRecord(@NonNull Context context, @NonNull MegaphoneRecord record) {
|
||||
switch (record.getEvent()) {
|
||||
case REACTIONS:
|
||||
return buildReactionsMegaphone();
|
||||
case PINS_FOR_ALL:
|
||||
return buildPinsForAllMegaphone(context, record);
|
||||
default:
|
||||
throw new IllegalArgumentException("Event not handled!");
|
||||
}
|
||||
|
@ -87,13 +92,65 @@ public final class Megaphones {
|
|||
|
||||
private static @NonNull Megaphone buildReactionsMegaphone() {
|
||||
return new Megaphone.Builder(Event.REACTIONS, Megaphone.Style.REACTIONS)
|
||||
.setMaxAppearances(Megaphone.UNLIMITED)
|
||||
.setMandatory(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull Context context, @NonNull MegaphoneRecord record) {
|
||||
if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) {
|
||||
return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN)
|
||||
.setMandatory(true)
|
||||
.enableSnooze(null)
|
||||
.setOnVisibleListener((megaphone, listener) -> {
|
||||
if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) {
|
||||
listener.onMegaphoneNavigationRequested(KbsMigrationActivity.createIntent(), KbsMigrationActivity.REQUEST_NEW_PIN);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
} else {
|
||||
Megaphone.Builder builder = new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.BASIC)
|
||||
.setMandatory(true)
|
||||
.setImage(R.drawable.kbs_pin_megaphone);
|
||||
|
||||
long daysRemaining = PinsForAllSchedule.getDaysRemaining(record.getFirstVisible(), System.currentTimeMillis());
|
||||
|
||||
if (PinUtil.userHasPin(ApplicationDependencies.getApplication())) {
|
||||
return buildPinsForAllMegaphoneForUserWithPin(
|
||||
builder.enableSnooze((megaphone, listener) -> listener.onMegaphoneToastRequested(context.getString(R.string.KbsMegaphone__well_remind_you_later_confirming_your_pin, daysRemaining)))
|
||||
);
|
||||
} else {
|
||||
return buildPinsForAllMegaphoneForUserWithoutPin(
|
||||
builder.enableSnooze((megaphone, listener) -> listener.onMegaphoneToastRequested(context.getString(R.string.KbsMegaphone__well_remind_you_later_creating_a_pin, daysRemaining)))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithPin(@NonNull Megaphone.Builder builder) {
|
||||
return builder.setTitle(R.string.KbsMegaphone__introducing_pins)
|
||||
.setBody(R.string.KbsMegaphone__your_registration_lock_is_now_called_a_pin)
|
||||
.setButtonText(R.string.KbsMegaphone__update_pin, (megaphone, listener) -> {
|
||||
Intent intent = CreateKbsPinActivity.getIntentForPinUpdate(ApplicationDependencies.getApplication());
|
||||
|
||||
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildPinsForAllMegaphoneForUserWithoutPin(@NonNull Megaphone.Builder builder) {
|
||||
return builder.setTitle(R.string.KbsMegaphone__create_a_pin)
|
||||
.setBody(R.string.KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account)
|
||||
.setButtonText(R.string.KbsMegaphone__create_pin, (megaphone, listener) -> {
|
||||
Intent intent = CreateKbsPinActivity.getIntentForPinCreate(ApplicationDependencies.getApplication());
|
||||
|
||||
listener.onMegaphoneNavigationRequested(intent, CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
public enum Event {
|
||||
REACTIONS("reactions");
|
||||
REACTIONS("reactions"),
|
||||
PINS_FOR_ALL("pins_for_all");
|
||||
|
||||
private final String key;
|
||||
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class PinsForAllSchedule implements MegaphoneSchedule {
|
||||
|
||||
@VisibleForTesting
|
||||
static final long DAYS_UNTIL_FULLSCREEN = 8L;
|
||||
|
||||
@VisibleForTesting
|
||||
static final long DAYS_REMAINING_MAX = DAYS_UNTIL_FULLSCREEN - 1;
|
||||
|
||||
private final MegaphoneSchedule schedule = new RecurringSchedule(TimeUnit.DAYS.toMillis(2));
|
||||
private final boolean enabled = !SignalStore.registrationValues().isPinRequired() || FeatureFlags.pinsForAll();
|
||||
|
||||
static boolean shouldDisplayFullScreen(long firstVisible, long currentTime) {
|
||||
if (firstVisible == 0L) {
|
||||
return false;
|
||||
} else {
|
||||
return currentTime - firstVisible >= TimeUnit.DAYS.toMillis(DAYS_UNTIL_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
static long getDaysRemaining(long firstVisible, long currentTime) {
|
||||
if (firstVisible == 0L) {
|
||||
return DAYS_REMAINING_MAX;
|
||||
} else {
|
||||
return Util.clamp(DAYS_REMAINING_MAX - TimeUnit.MILLISECONDS.toDays(currentTime - firstVisible), 0, DAYS_REMAINING_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
|
||||
if (!enabled) return false;
|
||||
|
||||
if (shouldDisplayFullScreen(firstVisible, currentTime)) {
|
||||
return true;
|
||||
} else {
|
||||
return schedule.shouldDisplay(seenCount, lastSeen, firstVisible, currentTime);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import androidx.preference.Preference;
|
|||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
|
@ -174,6 +175,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment {
|
|||
break;
|
||||
case SUCCESS:
|
||||
TextSecurePreferences.setPushRegistered(getActivity(), false);
|
||||
SignalStore.registrationValues().clearRegistrationComplete();
|
||||
initializePushMessagingToggle();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.BlockedContactsActivity;
|
||||
|
@ -23,8 +25,11 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
|||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
|
@ -45,11 +50,19 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
|
||||
disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary");
|
||||
|
||||
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
|
||||
regLock.setChecked(
|
||||
TextSecurePreferences.isV1RegistrationLockEnabled(requireContext()) || SignalStore.kbsValues().isV2RegistrationLockEnabled()
|
||||
);
|
||||
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
|
||||
SwitchPreferenceCompat regLock = (SwitchPreferenceCompat) this.findPreference(TextSecurePreferences.REGISTRATION_LOCK_PREF_V1);
|
||||
Preference kbsPinChange = this.findPreference(TextSecurePreferences.KBS_PIN_CHANGE);
|
||||
Preference regGroup = this.findPreference("prefs_lock_v1");
|
||||
Preference kbsGroup = this.findPreference("prefs_lock_v2");
|
||||
|
||||
if (FeatureFlags.pinsForAll()) {
|
||||
regGroup.setVisible(false);
|
||||
kbsPinChange.setOnPreferenceClickListener(new KbsPinChangeListener());
|
||||
} else {
|
||||
kbsGroup.setVisible(false);
|
||||
regLock.setChecked(PinUtil.userHasPin(requireContext()));
|
||||
regLock.setOnPreferenceClickListener(new AccountLockClickListener());
|
||||
}
|
||||
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener());
|
||||
this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener());
|
||||
|
@ -84,6 +97,13 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
disablePassphrase.setChecked(!TextSecurePreferences.isPasswordDisabled(getActivity()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) {
|
||||
Snackbar.make(requireView(), R.string.ConfirmKbsPinFragment__pin_created, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePassphraseTimeoutSummary() {
|
||||
int timeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(getActivity());
|
||||
this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF)
|
||||
|
@ -151,12 +171,20 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
}
|
||||
}
|
||||
|
||||
private class KbsPinChangeListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
startActivityForResult(CreateKbsPinActivity.getIntentForPinUpdate(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class AccountLockClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Context context = requireContext();
|
||||
|
||||
if (TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled()) {
|
||||
if (PinUtil.userHasPin(context)) {
|
||||
RegistrationLockDialog.showRegistrationUnlockPrompt(context, (SwitchPreferenceCompat)preference);
|
||||
} else {
|
||||
RegistrationLockDialog.showRegistrationLockPrompt(context, (SwitchPreferenceCompat)preference);
|
||||
|
@ -222,7 +250,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment
|
|||
final int privacySummaryResId = R.string.ApplicationPreferencesActivity_privacy_summary;
|
||||
final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on);
|
||||
final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off);
|
||||
boolean registrationLockEnabled = TextSecurePreferences.isV1RegistrationLockEnabled(context) || SignalStore.kbsValues().isV2RegistrationLockEnabled();
|
||||
boolean registrationLockEnabled = PinUtil.userHasPin(context);
|
||||
|
||||
if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) {
|
||||
if (registrationLockEnabled) {
|
||||
|
|
|
@ -35,6 +35,7 @@ import com.dd.CircularProgressButton;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
|
@ -321,6 +322,9 @@ public class EditProfileFragment extends Fragment {
|
|||
Log.w(TAG, "Failed to delete capture file " + captureFile);
|
||||
}
|
||||
}
|
||||
|
||||
SignalStore.registrationValues().setRegistrationComplete();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) handleFinishedLollipop();
|
||||
else handleFinishedLegacy();
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class AccountLockedFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.account_locked_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
AccountLockedFragmentArgs args = AccountLockedFragmentArgs.fromBundle(requireArguments());
|
||||
|
||||
TextView description = view.findViewById(R.id.account_locked_description);
|
||||
|
||||
description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, args.getTimeRemaining()));
|
||||
|
||||
view.findViewById(R.id.account_locked_next).setOnClickListener(this::onNextClicked);
|
||||
view.findViewById(R.id.account_locked_learn_more).setOnClickListener(this::onLearnMoreClicked);
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onNext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onNextClicked(@NonNull View unused) {
|
||||
onNext();
|
||||
}
|
||||
|
||||
private void onLearnMoreClicked(@NonNull View unused) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
|
||||
intent.setData(Uri.parse(getString(R.string.AccountLockedFragment__learn_more_url)));
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void onNext() {
|
||||
requireActivity().finish();
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
|
|||
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
|
@ -126,8 +127,13 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
|
|||
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean r) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
|
||||
if (FeatureFlags.pinsForAll()) {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining));
|
||||
} else {
|
||||
Navigation.findNavController(requireView())
|
||||
.navigate(EnterCodeFragmentDirections.actionRequireRegistrationLockPin(timeRemaining));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
package org.thoughtcrime.securesms.registration.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType;
|
||||
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
|
||||
import org.thoughtcrime.securesms.registration.service.RegistrationService;
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class KbsLockFragment extends BaseRegistrationFragment {
|
||||
|
||||
private EditText pinEntry;
|
||||
private CircularProgressButton pinButton;
|
||||
private TextView errorLabel;
|
||||
private TextView keyboardToggle;
|
||||
private long timeRemaining;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.kbs_lock_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title));
|
||||
|
||||
pinEntry = view.findViewById(R.id.kbs_lock_pin_input);
|
||||
pinButton = view.findViewById(R.id.kbs_lock_pin_confirm);
|
||||
errorLabel = view.findViewById(R.id.kbs_lock_pin_input_label);
|
||||
keyboardToggle = view.findViewById(R.id.kbs_lock_keyboard_toggle);
|
||||
|
||||
View pinForgotButton = view.findViewById(R.id.kbs_lock_forgot_pin);
|
||||
|
||||
timeRemaining = KbsLockFragmentArgs.fromBundle(requireArguments()).getTimeRemaining();
|
||||
|
||||
pinForgotButton.setOnClickListener(v -> handleForgottenPin(timeRemaining));
|
||||
|
||||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
hideKeyboard(requireContext(), v);
|
||||
handlePinEntry();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
pinButton.setOnClickListener((v) -> {
|
||||
hideKeyboard(requireContext(), pinEntry);
|
||||
handlePinEntry();
|
||||
});
|
||||
|
||||
keyboardToggle.setOnClickListener((v) -> {
|
||||
KbsKeyboardType keyboardType = getPinEntryKeyboardType();
|
||||
|
||||
updateKeyboard(keyboardType);
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
model.getTokenResponseCredentialsPair().observe(getViewLifecycleOwner(), pair -> {
|
||||
if (pair.first().getTries() == 0) {
|
||||
lockAccount();
|
||||
}
|
||||
});
|
||||
|
||||
model.onRegistrationLockFragmentCreate();
|
||||
}
|
||||
|
||||
private KbsKeyboardType getPinEntryKeyboardType() {
|
||||
boolean isNumeric = (pinEntry.getImeOptions() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
|
||||
|
||||
return isNumeric ? KbsKeyboardType.NUMERIC : KbsKeyboardType.ALPHA_NUMERIC;
|
||||
}
|
||||
|
||||
private void handlePinEntry() {
|
||||
final String pin = pinEntry.getText().toString();
|
||||
|
||||
if (TextUtils.isEmpty(pin) || TextUtils.isEmpty(pin.replace(" ", ""))) {
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
RegistrationViewModel model = getModel();
|
||||
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
|
||||
String storageCredentials = model.getBasicStorageCredentials();
|
||||
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
|
||||
|
||||
setSpinning(pinButton);
|
||||
|
||||
registrationService.verifyAccount(requireActivity(),
|
||||
model.getFcmToken(),
|
||||
model.getTextCodeEntered(),
|
||||
pin, storageCredentials, tokenResponse,
|
||||
|
||||
new CodeVerificationRequest.VerifyCallback() {
|
||||
|
||||
@Override
|
||||
public void onSuccessfulRegistration() {
|
||||
cancelSpinning(pinButton);
|
||||
SignalStore.kbsValues().setKeyboardType(getPinEntryKeyboardType());
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(KbsLockFragmentDirections.actionSuccessfulRegistration());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectRegistrationLockPin(long timeRemaining, String storageCredentials) {
|
||||
model.setStorageCredentials(storageCredentials);
|
||||
cancelSpinning(pinButton);
|
||||
|
||||
pinEntry.setText("");
|
||||
errorLabel.setText(R.string.KbsLockFragment__incorrect_pin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
|
||||
cancelSpinning(pinButton);
|
||||
|
||||
model.setKeyBackupCurrentToken(tokenResponse);
|
||||
|
||||
int triesRemaining = tokenResponse.getTries();
|
||||
|
||||
if (triesRemaining == 0) {
|
||||
lockAccount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (triesRemaining == 3) {
|
||||
long daysRemaining = getLockoutDays(timeRemaining);
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.KbsLockFragment__incorrect_pin)
|
||||
.setMessage(getString(R.string.KbsLockFragment__you_have_d_attempts_remaining, triesRemaining, daysRemaining, daysRemaining))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
if (triesRemaining > 5) {
|
||||
errorLabel.setText(R.string.KbsLockFragment__incorrect_pin_try_again);
|
||||
} else {
|
||||
errorLabel.setText(getString(R.string.KbsLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTooManyAttempts() {
|
||||
cancelSpinning(pinButton);
|
||||
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.RegistrationActivity_too_many_attempts)
|
||||
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
cancelSpinning(pinButton);
|
||||
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleForgottenPin(long timeRemainingMs) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.KbsLockFragment__forgot_your_pin)
|
||||
.setMessage(getString(R.string.KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, getLockoutDays(timeRemainingMs)))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private long getLockoutDays(long timeRemainingMs) {
|
||||
return TimeUnit.MILLISECONDS.toDays(timeRemainingMs) + 1;
|
||||
}
|
||||
|
||||
private void lockAccount() {
|
||||
KbsLockFragmentDirections.ActionAccountLocked action = KbsLockFragmentDirections.actionAccountLocked(timeRemaining);
|
||||
|
||||
Navigation.findNavController(requireView()).navigate(action);
|
||||
}
|
||||
|
||||
private void updateKeyboard(@NonNull KbsKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == KbsKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
}
|
||||
|
||||
private @StringRes int resolveKeyboardToggleText(@NonNull KbsKeyboardType keyboard) {
|
||||
if (keyboard == KbsKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.KbsLockFragment__enter_alphanumeric_pin;
|
||||
} else {
|
||||
return R.string.KbsLockFragment__enter_numeric_pin;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,11 @@ import androidx.navigation.ActivityNavigator;
|
|||
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinUtil;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
|
||||
|
||||
|
@ -30,12 +34,19 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
|
|||
|
||||
FragmentActivity activity = requireActivity();
|
||||
|
||||
|
||||
if (!isReregister()) {
|
||||
Intent setProfileNameIntent = getRoutedIntent(activity, EditProfileActivity.class, new Intent(activity, MainActivity.class));
|
||||
final Intent main = new Intent(activity, MainActivity.class);
|
||||
final Intent next = getRoutedIntent(activity, EditProfileActivity.class, main);
|
||||
|
||||
setProfileNameIntent.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
|
||||
next.putExtra(EditProfileActivity.SHOW_TOOLBAR, false);
|
||||
|
||||
activity.startActivity(setProfileNameIntent);
|
||||
Context context = requireContext();
|
||||
if (FeatureFlags.pinsForAll() && !PinUtil.userHasPin(context)) {
|
||||
activity.startActivity(getRoutedIntent(activity, CreateKbsPinActivity.class, next));
|
||||
} else {
|
||||
activity.startActivity(next);
|
||||
}
|
||||
}
|
||||
|
||||
activity.finish();
|
||||
|
|
|
@ -52,7 +52,7 @@ public final class FeatureFlags {
|
|||
private static final String USERNAMES = generateKey("usernames");
|
||||
private static final String KBS = generateKey("kbs");
|
||||
private static final String STORAGE_SERVICE = generateKey("storageService");
|
||||
private static final String REACTION_SENDING = generateKey("reactionSending");
|
||||
private static final String PINS_FOR_ALL = generateKey("beta.pinsForAll"); // TODO [alex] remove beta prefix
|
||||
|
||||
/**
|
||||
* Values in this map will take precedence over any value. If you do not wish to have any sort of
|
||||
|
@ -82,6 +82,7 @@ public final class FeatureFlags {
|
|||
* Flags in this set will stay true forever once they receive a true value from a remote config.
|
||||
*/
|
||||
private static final Set<String> STICKY = Sets.newHashSet(
|
||||
PINS_FOR_ALL // TODO [alex] -- add android.beta.pinsForAll to sticky set when we remove prefix
|
||||
);
|
||||
|
||||
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>();
|
||||
|
@ -153,9 +154,9 @@ public final class FeatureFlags {
|
|||
return value;
|
||||
}
|
||||
|
||||
/** Send support for reactions. */
|
||||
public static synchronized boolean reactionSending() {
|
||||
return getValue(REACTION_SENDING, false);
|
||||
/** Enables new KBS UI and notices but does not require user to set a pin */
|
||||
public static boolean pinsForAll() {
|
||||
return SignalStore.registrationValues().pinWasRequiredAtRegistration() || getValue(PINS_FOR_ALL, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
public final class RequestCodes {
|
||||
|
||||
public static final int NOT_SET = -1;
|
||||
|
||||
private RequestCodes() { }
|
||||
}
|
|
@ -17,6 +17,7 @@ import org.greenrobot.eventbus.EventBus;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsKeyboardType;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
|
@ -163,6 +164,8 @@ public class TextSecurePreferences {
|
|||
private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME_POST_KBS = "pref_registration_lock_last_reminder_time_post_kbs";
|
||||
private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval";
|
||||
|
||||
public static final String KBS_PIN_CHANGE = "pref_kbs_change";
|
||||
|
||||
private static final String SERVICE_OUTAGE = "pref_service_outage";
|
||||
private static final String LAST_OUTAGE_CHECK_TIME = "pref_last_outage_check_time";
|
||||
|
||||
|
|
|
@ -509,6 +509,10 @@ public class Util {
|
|||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
public static long clamp(long value, long min, long max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
public static float clamp(float value, float min, float max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package org.thoughtcrime.securesms.util.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class SlideUpWithSnackbarBehavior extends CoordinatorLayout.Behavior<View> {
|
||||
|
||||
public SlideUpWithSnackbarBehavior(@NonNull Context context, @Nullable AttributeSet attributeSet) {
|
||||
super(context, attributeSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent,
|
||||
@NonNull View child,
|
||||
@NonNull View dependency)
|
||||
{
|
||||
float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight());
|
||||
child.setTranslationY(translationY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDependentViewRemoved(@NonNull CoordinatorLayout parent,
|
||||
@NonNull View child,
|
||||
@NonNull View dependency)
|
||||
{
|
||||
child.setTranslationY(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent,
|
||||
@NonNull View child,
|
||||
@NonNull View dependency)
|
||||
{
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
}
|
BIN
app/src/main/res/drawable-mdpi/kbs_pin_megaphone.webp
Normal file
BIN
app/src/main/res/drawable-mdpi/kbs_pin_megaphone.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/kbs_pin_megaphone.webp
Normal file
BIN
app/src/main/res/drawable-xhdpi/kbs_pin_megaphone.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
BIN
app/src/main/res/drawable-xxhdpi/kbs_pin_megaphone.webp
Normal file
BIN
app/src/main/res/drawable-xxhdpi/kbs_pin_megaphone.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/kbs_pin_megaphone.webp
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/kbs_pin_megaphone.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
21
app/src/main/res/drawable/ic_kbs_splash_dark_svg.xml
Normal file
21
app/src/main/res/drawable/ic_kbs_splash_dark_svg.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="42.7dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="427">
|
||||
<path
|
||||
android:pathData="M284.256,346.041C329.141,315.766 305.914,195.351 259.927,127.173C213.94,58.994 118.155,37.768 73.271,68.043C28.386,98.318 29.28,178.13 75.266,246.308C121.253,314.486 239.372,376.316 284.256,346.041Z"
|
||||
android:fillColor="#2B2C2D"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M153.661,51.211C158.713,36.511 174.733,28.698 189.427,33.767L285.081,66.769C299.75,71.829 307.547,87.816 302.504,102.491L215.191,356.568C210.139,371.268 194.119,379.081 179.425,374.012L83.771,341.01C69.102,335.95 61.305,319.963 66.348,305.288L153.661,51.211Z"
|
||||
android:fillColor="#747474"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M199.07,43.701L185.209,38.928C175.42,35.557 164.751,40.761 161.38,50.55L72.196,309.562C68.825,319.351 74.028,330.02 83.818,333.391L184.409,368.027C194.199,371.398 204.868,366.195 208.239,356.405L297.423,97.394C300.794,87.604 295.591,76.936 285.801,73.565L272.352,68.934L272.148,69.527C270.238,75.074 264.192,78.023 258.644,76.113L205.452,57.797C199.904,55.887 196.956,49.841 198.866,44.294L199.07,43.701Z"
|
||||
android:fillColor="#363636"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M160.279,173.86L158.361,172.625L159.091,171.652L160.789,173.229L161.389,170.86L162.559,171.263L161.545,173.531L163.832,173.345L163.801,174.583L161.507,174.337L162.345,176.556L161.157,176.918L160.714,174.542L158.923,176.082L158.193,175.095L160.279,173.86ZM173.312,178.351L171.394,177.116L172.124,176.143L173.822,177.72L174.422,175.351L175.592,175.754L174.578,178.022L176.865,177.836L176.834,179.074L174.54,178.828L175.378,181.047L174.19,181.409L173.747,179.033L171.956,180.573L171.226,179.586L173.312,178.351ZM186.345,182.842L184.427,181.607L185.157,180.634L186.855,182.211L187.455,179.842L188.625,180.245L187.611,182.513L189.898,182.327L189.867,183.565L187.573,183.319L188.411,185.538L187.223,185.9L186.78,183.524L184.989,185.064L184.259,184.077L186.345,182.842ZM199.378,187.333L197.46,186.098L198.19,185.125L199.888,186.702L200.488,184.333L201.658,184.736L200.644,187.004L202.931,186.818L202.9,188.056L200.606,187.81L201.444,190.029L200.256,190.391L199.813,188.015L198.022,189.555L197.292,188.568L199.378,187.333ZM212.411,191.824L210.493,190.589L211.223,189.615L212.921,191.193L213.521,188.824L214.691,189.227L213.678,191.495L215.964,191.309L215.933,192.547L213.639,192.301L214.477,194.52L213.289,194.882L212.846,192.506L211.055,194.046L210.325,193.059L212.411,191.824ZM225.444,196.315L223.526,195.08L224.256,194.106L225.954,195.684L226.554,193.315L227.724,193.718L226.711,195.986L228.997,195.8L228.966,197.038L226.672,196.792L227.51,199.011L226.322,199.373L225.879,196.997L224.088,198.537L223.358,197.55L225.444,196.315Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
44
app/src/main/res/drawable/ic_kbs_splash_light_svg.xml
Normal file
44
app/src/main/res/drawable/ic_kbs_splash_light_svg.xml
Normal file
|
@ -0,0 +1,44 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="41.2dp"
|
||||
android:viewportWidth="360"
|
||||
android:viewportHeight="412">
|
||||
<group>
|
||||
<clip-path android:pathData="M-8,0h377v411.552h-377z M 0,0"/>
|
||||
<path
|
||||
android:pathData="M283.84,345.384C328.545,315.231 305.411,195.299 259.608,127.394C213.806,59.489 118.405,38.348 73.701,68.501C28.996,98.655 29.886,178.147 75.688,246.052C121.491,313.956 239.136,375.538 283.84,345.384Z"
|
||||
android:strokeAlpha="0.760579"
|
||||
android:fillColor="#D8EBFA"
|
||||
android:fillType="evenOdd"
|
||||
android:fillAlpha="0.760579"/>
|
||||
<path
|
||||
android:pathData="M153.772,51.718C158.8,37.087 174.744,29.311 189.368,34.356L284.678,67.239C299.277,72.276 307.037,88.186 302.018,102.792L215.041,355.889C210.014,370.519 194.07,378.296 179.446,373.25L84.135,340.368C69.536,335.331 61.776,319.42 66.795,304.815L153.772,51.718Z"
|
||||
android:fillColor="#F2F2F2"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M198.992,44.257L185.174,39.499C175.431,36.144 164.813,41.323 161.458,51.066L72.622,309.065C69.267,318.808 74.446,329.426 84.189,332.781L184.403,367.288C194.146,370.643 204.765,365.464 208.119,355.721L296.955,97.722C300.31,87.979 295.132,77.361 285.388,74.006L271.98,69.389L271.774,69.987C269.873,75.508 263.856,78.443 258.335,76.541L205.341,58.294C199.819,56.393 196.885,50.376 198.786,44.855L198.992,44.257Z"
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M142.531,251.916L134.043,256.955L126.667,276.331L129.465,283.601L119.591,310.688L95.268,316.218L101.109,265.665L121.09,232.022L144.796,244.074V248.681L176.661,247.262L223.199,268.301L220.565,292.54L198.435,346.36L182.383,331.438L192.682,305.122L198.435,299.877L200.421,277.414L178.651,250.288H165.372L142.531,251.916Z"
|
||||
android:fillColor="#E8E8E8"
|
||||
android:fillAlpha="0.01"
|
||||
android:fillType="evenOdd"/>
|
||||
<group>
|
||||
<clip-path android:pathData="M142.531,251.916L134.043,256.955L126.667,276.331L129.465,283.601L119.591,310.688L95.268,316.218L101.109,265.665L121.09,232.022L144.796,244.074V248.681L176.661,247.262L223.199,268.301L220.565,292.54L198.435,346.36L182.383,331.438L192.682,305.122L198.435,299.877L200.421,277.414L178.651,250.288H165.372L142.531,251.916Z M 0,0"/>
|
||||
<path
|
||||
android:pathData="M151.889,249.45L131.601,256.264L132.67,271.55L127.988,276.988L128.888,289.856L120.682,316.955L183.106,337.45L194.187,301.799L199.322,294.51L199.836,262.966L176.641,244.987L151.889,249.45Z"
|
||||
android:fillColor="#E8E8E8"
|
||||
android:fillAlpha="0.01"
|
||||
android:fillType="evenOdd"/>
|
||||
<group>
|
||||
<clip-path android:pathData="M151.889,249.45L131.601,256.264L132.67,271.55L127.988,276.988L128.888,289.856L120.682,316.955L183.106,337.45L194.187,301.799L199.322,294.51L199.836,262.966L176.641,244.987L151.889,249.45Z M 0,0"/>
|
||||
<path
|
||||
android:pathData="M124.227,234.022l100.912,34.747l-25.498,74.051l-100.912,-34.747z"/>
|
||||
</group>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M160.36,173.85L158.452,172.622L159.178,171.653L160.868,173.222L161.466,170.864L162.63,171.265L161.621,173.523L163.897,173.337L163.866,174.569L161.583,174.325L162.416,176.533L161.234,176.893L160.793,174.528L159.01,176.062L158.283,175.08L160.36,173.85ZM173.332,178.317L171.424,177.089L172.15,176.119L173.84,177.689L174.438,175.331L175.602,175.732L174.593,177.99L176.869,177.804L176.838,179.036L174.555,178.791L175.388,180.999L174.206,181.359L173.765,178.995L171.982,180.528L171.255,179.547L173.332,178.317ZM186.304,182.783L184.396,181.555L185.122,180.586L186.812,182.155L187.41,179.798L188.574,180.199L187.565,182.456L189.841,182.271L189.81,183.503L187.527,183.258L188.361,185.466L187.178,185.826L186.737,183.462L184.954,184.995L184.227,184.013L186.304,182.783ZM199.276,187.25L197.368,186.022L198.094,185.053L199.784,186.622L200.382,184.264L201.546,184.665L200.537,186.923L202.814,186.737L202.782,187.969L200.499,187.725L201.333,189.933L200.15,190.293L199.709,187.928L197.926,189.462L197.2,188.48L199.276,187.25ZM212.249,191.717L210.34,190.489L211.066,189.519L212.756,191.089L213.354,188.731L214.518,189.132L213.509,191.389L215.786,191.204L215.754,192.436L213.471,192.191L214.305,194.399L213.122,194.759L212.681,192.395L210.898,193.928L210.172,192.946L212.249,191.717ZM225.221,196.183L223.312,194.955L224.038,193.986L225.728,195.555L226.326,193.198L227.491,193.599L226.481,195.856L228.758,195.67L228.726,196.903L226.443,196.658L227.277,198.866L226.094,199.226L225.653,196.862L223.87,198.395L223.144,197.413L225.221,196.183Z"
|
||||
android:fillColor="#2090EA"/>
|
||||
</group>
|
||||
</vector>
|
63
app/src/main/res/layout/account_locked_fragment.xml
Normal file
63
app/src/main/res/layout/account_locked_fragment.xml
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_locked_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="49dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/AccountLockedFragment__account_locked"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_locked_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:minHeight="66dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/core_grey_60"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/account_locked_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/account_locked_next"
|
||||
style="@style/Button.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/cta_button_background"
|
||||
android:text="@string/AccountLockedFragment__next"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toTopOf="@id/account_locked_learn_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/account_locked_learn_more"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/AccountLockedFragment__learn_more"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
110
app/src/main/res/layout/base_kbs_pin_fragment.xml
Normal file
110
app/src/main/res/layout/base_kbs_pin_fragment.xml
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_kbs_pin_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/edit_kbs_pin_keyboard_toggle"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.20"
|
||||
tools:text="Create your PIN" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_kbs_pin_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:minHeight="66dp"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="?attr/title_text_color_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_title"
|
||||
tools:text="PINs add an extra layer of security to your account. Write your PIN down and keep it in a safe place. It can\'t be recovered." />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_kbs_pin_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:inputType="numberPassword"
|
||||
android:minWidth="210dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_description" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/edit_kbs_pin_lottie_progress"
|
||||
android:layout_width="57dp"
|
||||
android:layout_height="57dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_description" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/edit_kbs_pin_lottie_end"
|
||||
android:layout_width="57dp"
|
||||
android:layout_height="57dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_kbs_pin_input_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_input"
|
||||
app:layout_goneMarginTop="65dp"
|
||||
tools:text="PIN must be at least 6 digits" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_kbs_pin_keyboard_toggle"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintTop_toBottomOf="@id/edit_kbs_pin_input_label"
|
||||
app:layout_constraintBottom_toTopOf="@id/edit_kbs_pin_confirm"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:text="Create Alphanumeric Pin" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/edit_kbs_pin_confirm"
|
||||
style="@style/Button.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/cta_button_background"
|
||||
android:text="@string/BaseKbsPinFragment__next"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
|
@ -172,48 +172,63 @@
|
|||
tools:listitem="@layout/conversation_list_item_view"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/camera_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/conversation_list_fragment__open_camera_description"
|
||||
android:focusable="true"
|
||||
android:tint="?conversation_list_camera_icon_tint"
|
||||
app:backgroundTint="?conversation_list_camera_button_background"
|
||||
app:layout_constraintBottom_toTopOf="@id/fab"
|
||||
app:layout_constraintEnd_toEndOf="@id/fab"
|
||||
app:srcCompat="@drawable/ic_camera_solid_24" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
|
||||
android:focusable="true"
|
||||
android:tint="?conversation_list_compose_icon_tint"
|
||||
app:layout_constraintBottom_toTopOf="@id/megaphone_container"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_compose_solid_24" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/megaphone_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:clipToPadding="false"
|
||||
android:clipChildren="false"
|
||||
android:visibility="gone"
|
||||
app:contentPadding="0dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardPreventCornerOverlap="false"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:cardBackgroundColor="?megaphone_background"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinator"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="500dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior=".util.views.SlideUpWithSnackbarBehavior">
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/camera_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/conversation_list_fragment__open_camera_description"
|
||||
android:focusable="true"
|
||||
android:tint="?conversation_list_camera_icon_tint"
|
||||
app:backgroundTint="?conversation_list_camera_button_background"
|
||||
app:layout_constraintBottom_toTopOf="@id/fab"
|
||||
app:layout_constraintEnd_toEndOf="@id/fab"
|
||||
app:srcCompat="@drawable/ic_camera_solid_24" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
|
||||
android:focusable="true"
|
||||
android:tint="?conversation_list_compose_icon_tint"
|
||||
app:layout_constraintBottom_toTopOf="@id/megaphone_container"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_compose_solid_24" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/megaphone_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?megaphone_background"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardPreventCornerOverlap="false"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:contentPadding="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
17
app/src/main/res/layout/create_kbs_pin_activity.xml
Normal file
17
app/src/main/res/layout/create_kbs_pin_activity.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".lock.v2.CreateKbsPinActivity">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/create_kbs_pin" />
|
||||
|
||||
</FrameLayout>
|
95
app/src/main/res/layout/kbs_lock_fragment.xml
Normal file
95
app/src/main/res/layout/kbs_lock_fragment.xml
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/kbs_lock_pin_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/KbsLockFragment__enter_your_pin"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/kbs_lock_keyboard_toggle"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.20" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/kbs_lock_pin_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:minHeight="66dp"
|
||||
android:text="@string/KbsLockFragment__enter_the_pin_you_created"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/core_grey_60"
|
||||
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_title" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/kbs_lock_pin_input"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:inputType="numberPassword"
|
||||
android:minWidth="210dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/kbs_lock_pin_input_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_input"
|
||||
tools:text="@string/KbsLockFragment__incorrect_pin_try_again" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/kbs_lock_forgot_pin"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:text="@string/KbsLockFragment__forgot_pin"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/kbs_lock_pin_input" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/kbs_lock_keyboard_toggle"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintBottom_toTopOf="@id/kbs_lock_pin_confirm"
|
||||
tools:text="Create Alphanumeric Pin" />
|
||||
|
||||
<com.dd.CircularProgressButton
|
||||
android:id="@+id/kbs_lock_pin_confirm"
|
||||
style="@style/Button.Registration"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cpb_textIdle="@string/RegistrationActivity_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
17
app/src/main/res/layout/kbs_migration_activity.xml
Normal file
17
app/src/main/res/layout/kbs_migration_activity.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".lock.v2.KbsMigrationActivity">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/kbs_migration" />
|
||||
|
||||
</FrameLayout>
|
10
app/src/main/res/layout/kbs_pin_change_preference.xml
Normal file
10
app/src/main/res/layout/kbs_pin_change_preference.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:text="@string/preferences_app_protection__change" />
|
79
app/src/main/res/layout/kbs_pin_reminder_view.xml
Normal file
79
app/src/main/res/layout/kbs_pin_reminder_view.xml
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/header_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/signal_primary"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="40dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/KbsReminderDialog__enter_your_signal_pin"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/pin_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="80dp"
|
||||
android:paddingTop="40dp"
|
||||
android:paddingEnd="80dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/pin"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/registration_lock_reminder_view_pin_text_color" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.3"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingTop="40dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingBottom="40dp"
|
||||
android:textSize="15sp"
|
||||
tools:text="@string/KbsReminderDialog__to_help_you_memorize_your_pin" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/skip"
|
||||
style="@style/Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/KbsReminderDialog__skip" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/submit"
|
||||
style="@style/Button.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/KbsReminderDialog__submit" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
74
app/src/main/res/layout/kbs_splash_fragment.xml
Normal file
74
app/src/main/res/layout/kbs_splash_fragment.xml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/kbs_splash_image"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:srcCompat="?attr/kbs_splash_image"
|
||||
app:layout_constraintBottom_toTopOf="@id/kbs_splash_title"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/kbs_splash_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="7dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title1"
|
||||
app:layout_constraintBottom_toTopOf="@id/kbs_splash_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="@string/KbsSplashFragment__registration_lock_equals_pin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/kbs_splash_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="27dp"
|
||||
android:layout_marginEnd="27dp"
|
||||
android:layout_marginBottom="36dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="@color/core_grey_60"
|
||||
app:layout_constraintBottom_toTopOf="@id/kbs_splash_primary_action"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="@string/KbsSplashFragment__your_registration_lock_is_now_called_a_pin" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/kbs_splash_primary_action"
|
||||
style="@style/Button.Primary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/cta_button_background"
|
||||
android:text="@string/KbsSplashFragment__create_your_pin"
|
||||
android:textColor="@color/core_white"
|
||||
app:layout_constraintBottom_toTopOf="@id/kbs_splash_secondary_action"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/kbs_splash_secondary_action"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/KbsSplashFragment__learn_more"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
54
app/src/main/res/navigation/create_kbs_pin.xml
Normal file
54
app/src/main/res/navigation/create_kbs_pin.xml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/create_kbs_pin"
|
||||
app:startDestination="@id/createKbsPinFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/createKbsPinFragment"
|
||||
android:name="org.thoughtcrime.securesms.lock.v2.CreateKbsPinFragment"
|
||||
android:label="fragment_edit_kbs_pin"
|
||||
tools:layout="@layout/base_kbs_pin_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_confirmPin"
|
||||
app:destination="@id/confirmKbsPinFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
<argument
|
||||
app:argType="boolean"
|
||||
android:name="is_new_pin"
|
||||
android:defaultValue="false" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/confirmKbsPinFragment"
|
||||
android:name="org.thoughtcrime.securesms.lock.v2.ConfirmKbsPinFragment"
|
||||
android:label="fragment_confirm_new_pin"
|
||||
tools:layout="@layout/base_kbs_pin_fragment">
|
||||
|
||||
<argument
|
||||
app:argType="boolean"
|
||||
android:name="is_new_pin"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<argument
|
||||
android:name="user_entry"
|
||||
android:defaultValue="@null"
|
||||
app:argType="org.thoughtcrime.securesms.lock.v2.KbsPin"
|
||||
app:nullable="true" />
|
||||
|
||||
<argument
|
||||
android:name="keyboard"
|
||||
android:defaultValue="NUMERIC"
|
||||
app:argType="org.thoughtcrime.securesms.lock.v2.KbsKeyboardType"
|
||||
app:nullable="false" />
|
||||
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
26
app/src/main/res/navigation/kbs_migration.xml
Normal file
26
app/src/main/res/navigation/kbs_migration.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/kbs_pin_migration"
|
||||
app:startDestination="@id/kbsSplashFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/kbsSplashFragment"
|
||||
android:name="org.thoughtcrime.securesms.lock.v2.KbsSplashFragment"
|
||||
android:label="fragment_kbs_splash"
|
||||
tools:layout="@layout/kbs_splash_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_createKbsPin"
|
||||
app:destination="@id/create_kbs_pin"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<include app:graph="@navigation/create_kbs_pin" />
|
||||
|
||||
</navigation>
|
|
@ -91,6 +91,16 @@
|
|||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_requireKbsLockPin"
|
||||
app:destination="@id/kbsLockFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_wrongNumber"
|
||||
app:popUpTo="@id/enterCodeFragment"
|
||||
|
@ -134,6 +144,49 @@
|
|||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/kbsLockFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.KbsLockFragment"
|
||||
android:label="fragment_kbs_lock"
|
||||
tools:layout="@layout/kbs_lock_fragment">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_successfulRegistration"
|
||||
app:destination="@id/registrationCompletePlaceHolderFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_accountLocked"
|
||||
app:destination="@id/accountLockedFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
<argument
|
||||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/accountLockedFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
|
||||
android:label="fragment_account_locked"
|
||||
tools:layout="@layout/account_locked_fragment">
|
||||
|
||||
<argument
|
||||
android:name="timeRemaining"
|
||||
app:argType="long" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/captchaFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
|
||||
|
|
1
app/src/main/res/raw/lottie_kbs_failure.json
Normal file
1
app/src/main/res/raw/lottie_kbs_failure.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"v":"5.5.2","fr":60,"ip":0,"op":190,"w":100,"h":100,"nm":"Progress indicator - Indeterminate - End Fail","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check2 Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":114,"s":[0]},{"t":117,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,51,0],"ix":2},"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[4.019,-19.126],[3.135,7.112],[-3.163,7.112],[-3.991,-19.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.293,0],[0,-2.293],[2.32,0],[0,2.292]],"o":[[2.32,0],[0,2.292],[-2.293,0],[0,-2.293]],"v":[[-0.014,11.339],[4.184,15.232],[-0.014,19.126],[-4.185,15.232]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.125,0.564999988032,0.917999985639,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[50.047,48.874],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":190,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"1","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":184,"s":[100]},{"t":189,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-26]},{"t":81,"s":[0]}],"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.1254902035,0.564705908298,0.917647063732,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":96,"s":[0]},{"t":107,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[5.5]},{"t":96,"s":[100]}],"ix":2},"o":{"a":0,"k":1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":190,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Rotator","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":323,"s":[1444]}],"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":190,"st":0,"bm":0}],"markers":[]}
|
1
app/src/main/res/raw/lottie_kbs_loading.json
Normal file
1
app/src/main/res/raw/lottie_kbs_loading.json
Normal file
File diff suppressed because one or more lines are too long
1
app/src/main/res/raw/lottie_kbs_success.json
Normal file
1
app/src/main/res/raw/lottie_kbs_success.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"v":"5.5.2","fr":60,"ip":0,"op":189,"w":100,"h":100,"nm":"Progress indicator - Indeterminate - End success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"check Outlines","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":107,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":109,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":183,"s":[100]},{"t":188,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50.5,51,0],"ix":2},"a":{"a":0,"k":[50,50,0],"ix":1},"s":{"a":0,"k":[81,81,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-19.278,2.378],[-7.103,14.354],[19.278,-14.354]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.1254902035,0.564705908298,0.917647063732,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8.5,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[49.136,49.354],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":116,"s":[0]},{"t":122,"s":[100]}],"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":189,"st":-1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"1","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":183,"s":[100]},{"t":188,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-1,"s":[-26]},{"t":80,"s":[0]}],"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[70,70],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.1254902035,0.564705908298,0.917647063732,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":95,"s":[0]},{"t":106,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":-1,"s":[5.5]},{"t":95,"s":[100]}],"ix":2},"o":{"a":0,"k":1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":189,"st":-1,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"Rotator","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":-1,"s":[0]},{"t":322,"s":[1444]}],"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":189,"st":-1,"bm":0}],"markers":[]}
|
|
@ -31,6 +31,8 @@
|
|||
<attr name="conversation_list_compose_icon_tint" format="color" />
|
||||
<attr name="conversation_list_camera_button_background" format="color"/>
|
||||
|
||||
<attr name="kbs_splash_image" format="reference" />
|
||||
|
||||
<attr name="conversation_sent_card_background" format="reference|color"/>
|
||||
<attr name="conversation_group_member_name" format="reference|color"/>
|
||||
<attr name="conversation_received_card_background" format="reference|color"/>
|
||||
|
|
|
@ -1682,12 +1682,18 @@
|
|||
<string name="BaseKbsPinFragment__create_numeric_pin">Create numeric PIN</string>
|
||||
|
||||
<!-- CreateKbsPinFragment -->
|
||||
<string name="CreateKbsPinFragment__pin_must_be_at_least_characters">PIN must be at least %1$d characters</string>
|
||||
<string name="CreateKbsPinFragment__pin_must_be_at_least_digits">PIN must be at least %1$d digits</string>
|
||||
<plurals name="CreateKbsPinFragment__pin_must_be_at_least_characters">
|
||||
<item quantity="one">PIN must be at least %1$d character</item>
|
||||
<item quantity="other">PIN must be at least %1$d characters</item>
|
||||
</plurals>
|
||||
<plurals name="CreateKbsPinFragment__pin_must_be_at_least_digits">
|
||||
<item quantity="one">PIN must be at least %1$d digit</item>
|
||||
<item quantity="other">PIN must be at least %1$d digits</item>
|
||||
</plurals>
|
||||
<string name="CreateKbsPinFragment__create_a_new_pin">Create a new PIN</string>
|
||||
<string name="CreateKbsPinFragment__because_youre_still_logged_in">Because you\'re still logged in, you can create a new PIN. When you\'re logged out, there is no way to recover your PIN.</string>
|
||||
<string name="CreateKbsPinFragment__create_your_pin">Create your PIN</string>
|
||||
<string name="CreateKbsPinFragment__pins_add_an_extra_layer_of_security">PINs add an extra layer of security to your account. Write your PIN down and keep it in a safe place. It can\'t be recovered.</string>
|
||||
<string name="CreateKbsPinFragment__pins_add_an_extra_layer_of_security">PINs add an extra layer of security to your account. It\'s important to remember this PIN, as it can\'t be recovered.</string>
|
||||
|
||||
<!-- ConfirmKbsPinFragment -->
|
||||
<string name="ConfirmKbsPinFragment__pins_dont_match">PINs don\'t match. Try again.</string>
|
||||
|
@ -1695,18 +1701,19 @@
|
|||
<string name="ConfirmKbsPinFragment__pin_creation_failed">PIN creation failed</string>
|
||||
<string name="ConfirmKbsPinFragment__your_pin_was_not_saved">Your PIN was not saved. We\'ll prompt you to create a PIN later.</string>
|
||||
<string name="ConfirmKbsPinFragment__pin_created">PIN created.</string>
|
||||
<string name="ConfirmKbsPinFragment__re_enter_pin">Re-enter PIN</string>
|
||||
<string name="ConfirmKbsPinFragment__creating_pin">Creating PIN...</string>
|
||||
|
||||
<!-- KbsSplashFragment -->
|
||||
<string name="KbsSplashFragment__introducing_pins">Introducing PINs</string>
|
||||
<string name="KbsSplashFragment__add_another_level_of_security_to_your_account">Add another level of security to your account. %1$s</string>
|
||||
<string name="KbsSplashFragment__read_more_here">Read more here.</string>
|
||||
<string name="KbsSplashFragment__read_more_link">https://signal.org/blog/secure-value-recovery/</string>
|
||||
<string name="KbsSplashFragment__pins_add_another_level_of_security_to_your_account">PINs add another level of security to your account. Create one now.</string>
|
||||
<string name="KbsSplashFragment__learn_more">Learn More</string>
|
||||
<string name="KbsSplashFragment__learn_more_link">https://signal.org/blog/secure-value-recovery/</string>
|
||||
<string name="KbsSplashFragment__registration_lock_equals_pin">Registration Lock = PIN</string>
|
||||
<string name="KbsSplashFragment__your_registration_lock_is_now_called_a_pin">Your Registration Lock is now called a PIN, and it does more. Update it now. %1$s</string>
|
||||
<string name="KbsSplashFragment__your_registration_lock_is_now_called_a_pin">Your Registration Lock is now called a PIN, and it does more. Update it now.</string>
|
||||
<string name="KbsSplashFragment__read_more_about_pins">Read more about PINs.</string>
|
||||
<string name="KbsSplashFragment__update_pin">Update PIN</string>
|
||||
<string name="KbsSplashFragment__create_your_pin">Create your PIN</string>
|
||||
<string name="KbsSplashFragment__remind_me_later">Remind me later</string>
|
||||
|
||||
<!-- KBS Reminder Dialog -->
|
||||
<string name="KbsReminderDialog__enter_your_signal_pin">Enter your Signal PIN</string>
|
||||
|
@ -1735,7 +1742,17 @@
|
|||
<string name="KbsLockFragment__incorrect_pin">Incorrect PIN</string>
|
||||
<string name="KbsLockFragment__you_have_d_attempts_remaining">You have %1$d attempts remaining. If you run out of attempts your account will be locked for %2$d days. After %3$d days of inactivity, you can re-register without your PIN. Your account will be wiped and all content deleted.</string>
|
||||
<string name="KbsLockFragment__forgot_your_pin">Forgot your PIN?</string>
|
||||
<string name="KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover">For your privacy and security, there is no way to recover your PIN. If you run out of attempts, you can re-verify with SMS after %1$d days of inactivity. In this case, your account will be wiped and all content deleted.</string>
|
||||
<string name="KbsLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover">For your privacy and security, there is no way to recover your PIN. If you can\'t remember your PIN, you can re-verify with SMS after %1$d days of inactivity. In this case, your account will be wiped and all content deleted.</string>
|
||||
|
||||
<!-- KBS Megaphone -->
|
||||
<string name="KbsMegaphone__create_a_pin">Create a PIN</string>
|
||||
<string name="KbsMegaphone__pins_add_another_layer_of_security_to_your_signal_account">PINs add another layer of security to your Signal account.</string>
|
||||
<string name="KbsMegaphone__create_pin">Create PIN</string>
|
||||
<string name="KbsMegaphone__introducing_pins">Introducing PINs</string>
|
||||
<string name="KbsMegaphone__your_registration_lock_is_now_called_a_pin">Your Registration Lock is now called a PIN. Updating it takes seconds.</string>
|
||||
<string name="KbsMegaphone__update_pin">Update PIN</string>
|
||||
<string name="KbsMegaphone__well_remind_you_later_creating_a_pin">We\'ll remind you later. Creating a PIN will become mandatory in %1$d days.</string>
|
||||
<string name="KbsMegaphone__well_remind_you_later_confirming_your_pin">We\'ll remind you later. Confirming your PIN will become mandatory in %1$d days.</string>
|
||||
|
||||
<!-- transport_selection_list_item -->
|
||||
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
||||
|
|
|
@ -45,6 +45,12 @@
|
|||
<item name="android:textColor">@color/core_white</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Title1" parent="@style/TextAppearance.AppCompat.Title">
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">28sp</item>
|
||||
<item name="android:color">?attr/title_text_color_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Title2" parent="@style/TextAppearance.AppCompat.Title">
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
|
|
@ -188,6 +188,8 @@
|
|||
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||
<!--<item name="android:windowContentOverlay">@drawable/compat_actionbar_shadow_background</item>-->
|
||||
|
||||
<item name="kbs_splash_image">@drawable/ic_kbs_splash_light_svg</item>
|
||||
|
||||
<item name="attachment_type_selector_background">@color/white</item>
|
||||
<item name="attachment_document_icon_small">@drawable/ic_document_small_light</item>
|
||||
<item name="attachment_document_icon_large">@drawable/ic_document_large_light</item>
|
||||
|
@ -440,6 +442,8 @@
|
|||
<item name="homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||
<item name="android:homeAsUpIndicator">@drawable/ic_arrow_left_24</item>
|
||||
|
||||
<item name="kbs_splash_image">@drawable/ic_kbs_splash_dark_svg</item>
|
||||
|
||||
<item name="attachment_type_selector_background">@color/core_grey_95</item>
|
||||
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>
|
||||
<item name="attachment_document_icon_large">@drawable/ic_document_large_dark</item>
|
||||
|
@ -651,6 +655,15 @@
|
|||
<item name="android:windowBackground">@drawable/permission_rationale_dialog_corners</item>
|
||||
</style>
|
||||
|
||||
<style name="RationaleDialogLight.SignalAccent">
|
||||
<item name="colorAccent">@color/signal_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="RationaleDialogDark.SignalAccent">
|
||||
<item name="colorAccent">@color/signal_primary</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="Theme.Signal.Insights.Modal" parent="@style/Theme.AppCompat.Dialog.MinWidth">
|
||||
</style>
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
||||
<PreferenceCategory android:title="@string/preferences_app_protection__registration_lock">
|
||||
<PreferenceCategory android:key="prefs_lock_v1" android:title="@string/preferences_app_protection__registration_lock">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_registration_lock"
|
||||
|
@ -110,5 +110,12 @@
|
|||
android:summary="@string/preferences_app_protection__enable_a_registration_lock_pin_that_will_be_required"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:key="prefs_lock_v2" android:title="@string/preferences_app_protection__signal_pin">
|
||||
<Preference
|
||||
android:widgetLayout="@layout/kbs_pin_change_preference"
|
||||
android:key="pref_kbs_change"
|
||||
android:title="@string/preferences_app_protection__pin"
|
||||
android:summary="@string/preferences_app_protection__your_pin_adds_an_extra_layer_of_security_and_backs" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
import org.thoughtcrime.securesms.keyvalue.RegistrationValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
||||
import static org.powermock.api.mockito.PowerMockito.when;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({SignalStore.class, FeatureFlags.class, RegistrationValues.class})
|
||||
public class PinsForAllScheduleTest {
|
||||
|
||||
private final PinsForAllSchedule testSubject = new PinsForAllSchedule();
|
||||
private final RegistrationValues registrationValues = mock(RegistrationValues.class);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mockStatic(SignalStore.class);
|
||||
mockStatic(FeatureFlags.class);
|
||||
when(SignalStore.registrationValues()).thenReturn(registrationValues);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsZero_whenIShouldDisplayFullscreen_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
long firstVisible = 0;
|
||||
|
||||
// WHEN
|
||||
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(firstVisible, 0);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsNow_whenIShouldDisplayFullscreen_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// WHEN
|
||||
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(now, now);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsHalfFullscreenTimeout_whenIShouldDisplayFullscreen_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
long lastWeek = now - TimeUnit.DAYS.toMillis(PinsForAllSchedule.DAYS_UNTIL_FULLSCREEN / 2);
|
||||
|
||||
// WHEN
|
||||
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(lastWeek, now);
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsFullscreenTimeout_whenIShouldDisplayFullscreen_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
long lastWeek = now - TimeUnit.DAYS.toMillis(PinsForAllSchedule.DAYS_UNTIL_FULLSCREEN);
|
||||
|
||||
// WHEN
|
||||
boolean result = PinsForAllSchedule.shouldDisplayFullScreen(lastWeek, now);
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsZero_whenIGetDaysRemaining_thenIExpectMax() {
|
||||
// GIVEN
|
||||
long firstVisible = 0;
|
||||
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX;
|
||||
|
||||
// WHEN
|
||||
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, 0);
|
||||
|
||||
// THEN
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsNow_whenIGetDaysRemaining_thenIExpectMax() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX;
|
||||
|
||||
// WHEN
|
||||
long result = PinsForAllSchedule.getDaysRemaining(now, now);
|
||||
|
||||
// THEN
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsFiveSecondsAgo_whenIGetDaysRemaining_thenIExpectMax() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
long firstVisible = now - TimeUnit.SECONDS.toMillis(5);
|
||||
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX;
|
||||
|
||||
// WHEN
|
||||
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, now);
|
||||
|
||||
// THEN
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsADayAgo_whenIGetDaysRemaining_thenIExpectMaxLessOne() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
long firstVisible = now - TimeUnit.DAYS.toMillis(1);
|
||||
long expected = PinsForAllSchedule.DAYS_REMAINING_MAX - 1;
|
||||
|
||||
// WHEN
|
||||
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, now);
|
||||
|
||||
// THEN
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenFirstVisibleIsAMonthAgo_whenIGetDaysRemaining_thenIExpectZero() {
|
||||
// GIVEN
|
||||
long now = System.currentTimeMillis();
|
||||
long firstVisible = now - TimeUnit.DAYS.toMillis(31);
|
||||
long expected = 0;
|
||||
|
||||
// WHEN
|
||||
long result = PinsForAllSchedule.getDaysRemaining(firstVisible, now);
|
||||
|
||||
// THEN
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUserIsANewInstallAndFlagIsDisabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUserIsANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(true);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUserIsNotANewInstallAndFlagIsEnabled_whenIShouldDisplay_thenIExpectTrue() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
|
||||
|
||||
// THEN
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenUserIsNotANewInstallAndFlagIsNotEnabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(false);
|
||||
|
||||
// WHEN
|
||||
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenKillSwitchEnabled_whenIShouldDisplay_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
when(registrationValues.pinWasRequiredAtRegistration()).thenReturn(false);
|
||||
when(FeatureFlags.pinsForAll()).thenReturn(true);
|
||||
when(FeatureFlags.pinsForAllMegaphoneKillSwitch()).thenReturn(true);
|
||||
|
||||
// WHEN
|
||||
boolean result = testSubject.shouldDisplay(0, 0, 0, System.currentTimeMillis());
|
||||
|
||||
// THEN
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue