diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java b/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java index 14a2c116e3..5d4a84345b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java +++ b/app/src/main/java/org/thoughtcrime/securesms/AppInitialization.java @@ -33,7 +33,7 @@ public final class AppInitialization { TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION); TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode()); TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true); - TextSecurePreferences.setPasswordDisabled(context, true); + SignalStore.settings().setPassphraseDisabled(true); TextSecurePreferences.setReadReceiptsEnabled(context, true); TextSecurePreferences.setTypingIndicatorsEnabled(context, true); TextSecurePreferences.setHasSeenWelcomeScreen(context, false); @@ -54,7 +54,7 @@ public final class AppInitialization { SignalStore.onFirstEverAppLaunch(); SignalStore.onboarding().clearAll(); TextSecurePreferences.onPostBackupRestore(context); - TextSecurePreferences.setPasswordDisabled(context, true); + SignalStore.settings().setPassphraseDisabled(true); AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false)); AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false)); AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false)); @@ -73,7 +73,7 @@ public final class AppInitialization { TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION); TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode()); TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true); - TextSecurePreferences.setPasswordDisabled(context, true); + SignalStore.settings().setPassphraseDisabled(true); AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch(); SignalStore.onFirstEverAppLaunch(); AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 24633cd948..d699f3f131 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -376,12 +376,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE); TextSecurePreferences.setFirstInstallVersion(this, BuildConfig.CANONICAL_VERSION_CODE); - } else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 90) { + } else if (!SignalStore.settings().getPassphraseDisabled() && VersionTracker.getDaysSinceFirstInstalled(this) < 90) { Log.i(TAG, "Detected a new install that doesn't have passphrases disabled -- assuming bad initialization."); AppInitialization.onRepairFirstEverAppLaunch(this); - } else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 912) { + } else if (!SignalStore.settings().getPassphraseDisabled() && VersionTracker.getDaysSinceFirstInstalled(this) < 912) { Log.i(TAG, "Detected a not-recent install that doesn't have passphrases disabled -- disabling now."); - TextSecurePreferences.setPasswordDisabled(this, true); + SignalStore.settings().setPassphraseDisabled(true); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/BiometricDeviceAuthentication.kt b/app/src/main/java/org/thoughtcrime/securesms/BiometricDeviceAuthentication.kt index ea29d878c9..8fe5eb5394 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BiometricDeviceAuthentication.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/BiometricDeviceAuthentication.kt @@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil class BiometricDeviceAuthentication( private val biometricManager: BiometricManager, private val biometricPrompt: BiometricPrompt, - private val biometricPromptInfo: PromptInfo + private var biometricPromptInfo: PromptInfo ) { companion object { const val AUTHENTICATED = 1 @@ -69,6 +69,10 @@ class BiometricDeviceAuthentication( fun cancelAuthentication() { biometricPrompt.cancelAuthentication() } + + fun updatePromptInfo(promptInfo: PromptInfo) { + biometricPromptInfo = promptInfo + } } class BiometricDeviceLockContract : ActivityResultContract() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseChangeActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseChangeActivity.java index 316e4e105c..cc7666ca5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseChangeActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseChangeActivity.java @@ -29,9 +29,9 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.InvalidPassphraseException; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.TextSecurePreferences; /** * Activity for changing a user's local encryption passphrase. @@ -81,7 +81,7 @@ public class PassphraseChangeActivity extends PassphraseActivity { this.okButton.setOnClickListener(new OkButtonClickListener()); this.cancelButton.setOnClickListener(new CancelButtonClickListener()); - if (TextSecurePreferences.isPasswordDisabled(this)) { + if (SignalStore.settings().getPassphraseDisabled()) { this.originalPassphrase.setVisibility(View.GONE); } else { this.originalPassphrase.setVisibility(View.VISIBLE); @@ -97,7 +97,7 @@ public class PassphraseChangeActivity extends PassphraseActivity { String passphrase = (newText == null ? "" : newText.toString()); String passphraseRepeat = (repeatText == null ? "" : repeatText.toString()); - if (TextSecurePreferences.isPasswordDisabled(this)) { + if (SignalStore.settings().getPassphraseDisabled()) { original = MasterSecretUtil.UNENCRYPTED_PASSPHRASE; } @@ -142,7 +142,7 @@ public class PassphraseChangeActivity extends PassphraseActivity { protected MasterSecret doInBackground(String... params) { try { MasterSecret masterSecret = MasterSecretUtil.changeMasterSecretPassphrase(context, params[0], params[1]); - TextSecurePreferences.setPasswordDisabled(context, false); + SignalStore.settings().setPassphraseDisabled(false); return masterSecret; diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java index 7088ee3821..ab8b013b93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java @@ -20,7 +20,6 @@ import android.animation.Animator; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; -import android.graphics.PorterDuff; import android.os.Bundle; import android.text.Editable; import android.text.InputType; @@ -34,21 +33,18 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; -import android.view.animation.Animation; -import android.view.animation.BounceInterpolator; -import android.view.animation.TranslateAnimation; import android.view.inputmethod.EditorInfo; import android.widget.EditText; import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; +import com.airbnb.lottie.LottieAnimationView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.ThreadUtil; @@ -64,7 +60,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.DynamicIntroTheme; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.SupportEmailUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.views.LearnMoreTextView; import kotlin.Unit; @@ -79,14 +75,14 @@ public class PassphrasePromptActivity extends PassphraseActivity { private static final short AUTHENTICATE_REQUEST_CODE = 1007; private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown"; public static final String FROM_FOREGROUND = "from_foreground"; - private static final int HELP_COUNT_THRESHOLD = 3; private DynamicIntroTheme dynamicTheme = new DynamicIntroTheme(); private DynamicLanguage dynamicLanguage = new DynamicLanguage(); - private View passphraseAuthContainer; - private ImageView fingerprintPrompt; - private TextView lockScreenButton; + private View passphraseAuthContainer; + private LottieAnimationView unlockView; + private TextView lockScreenButton; + private LearnMoreTextView learnMoreText; private EditText passphraseText; private ImageButton showButton; @@ -134,14 +130,11 @@ public class PassphrasePromptActivity extends PassphraseActivity { setLockTypeVisibility(); - if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !hadFailure) { + if (SignalStore.settings().getScreenLockEnabled() && !authenticated && !hadFailure) { ThreadUtil.postToMain(resumeScreenLockRunnable); } hadFailure = false; - - fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp); - fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN); } @Override @@ -174,9 +167,6 @@ public class PassphrasePromptActivity extends PassphraseActivity { if (item.getItemId() == R.id.menu_submit_debug_logs) { handleLogSubmit(); return true; - } else if (item.getItemId() == R.id.menu_contact_support) { - sendEmailToSupport(); - return true; } return false; @@ -193,7 +183,7 @@ public class PassphrasePromptActivity extends PassphraseActivity { } else { Log.w(TAG, "Authentication failed"); hadFailure = true; - incrementAttemptCountAndShowHelpIfNecessary(); + showHelpDialog(); } } @@ -213,7 +203,6 @@ public class PassphrasePromptActivity extends PassphraseActivity { passphraseText.setText(""); passphraseText.setError( getString(R.string.PassphrasePromptActivity_invalid_passphrase_exclamation)); - incrementAttemptCountAndShowHelpIfNecessary(); } } @@ -223,7 +212,6 @@ public class PassphrasePromptActivity extends PassphraseActivity { MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); setMasterSecret(masterSecret); - SignalStore.misc().setLockScreenAttemptCount(0); } catch (InvalidPassphraseException e) { throw new AssertionError(e); } @@ -251,8 +239,9 @@ public class PassphrasePromptActivity extends PassphraseActivity { visibilityToggle = findViewById(R.id.button_toggle); passphraseText = findViewById(R.id.passphrase_edit); passphraseAuthContainer = findViewById(R.id.password_auth_container); - fingerprintPrompt = findViewById(R.id.fingerprint_auth_container); - lockScreenButton = findViewById(R.id.lock_screen_auth_container); + unlockView = findViewById(R.id.unlock_view); + lockScreenButton = findViewById(R.id.lock_screen_button); + learnMoreText = findViewById(R.id.learn_more_text); biometricManager = BiometricManager.from(this); biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener()); BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo @@ -276,34 +265,22 @@ public class PassphrasePromptActivity extends PassphraseActivity { passphraseText.setImeActionLabel(getString(R.string.prompt_passphrase_activity__unlock), EditorInfo.IME_ACTION_DONE); - fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp); - fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN); - lockScreenButton.setOnClickListener(v -> resumeScreenLock(true)); - - if (SignalStore.misc().getLockScreenAttemptCount() > HELP_COUNT_THRESHOLD) { - showHelpDialogAndResetAttemptCount(null); - } } private void setLockTypeVisibility() { - if (TextSecurePreferences.isScreenLockEnabled(this)) { + if (SignalStore.settings().getScreenLockEnabled()) { passphraseAuthContainer.setVisibility(View.GONE); - fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BiometricDeviceAuthentication.BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE - : View.GONE); + unlockView.setVisibility(View.VISIBLE); lockScreenButton.setVisibility(View.VISIBLE); } else { passphraseAuthContainer.setVisibility(View.VISIBLE); - fingerprintPrompt.setVisibility(View.GONE); + unlockView.setVisibility(View.GONE); lockScreenButton.setVisibility(View.GONE); } } private void resumeScreenLock(boolean force) { - if (incrementAttemptCountAndShowHelpIfNecessary(() -> resumeScreenLock(force))) { - return; - } - if (!biometricAuth.authenticate(getApplicationContext(), force, this::showConfirmDeviceCredentialIntent)) { handleAuthenticated(); } @@ -328,33 +305,6 @@ public class PassphrasePromptActivity extends PassphraseActivity { return Unit.INSTANCE; } - private boolean incrementAttemptCountAndShowHelpIfNecessary() { - return incrementAttemptCountAndShowHelpIfNecessary(null); - } - - private boolean incrementAttemptCountAndShowHelpIfNecessary(Runnable onDismissed) { - SignalStore.misc().incrementLockScreenAttemptCount(); - - if (SignalStore.misc().getLockScreenAttemptCount() > HELP_COUNT_THRESHOLD) { - showHelpDialogAndResetAttemptCount(onDismissed); - return true; - } - - return false; - } - - private void showHelpDialogAndResetAttemptCount(@Nullable Runnable onDismissed) { - new MaterialAlertDialogBuilder(this) - .setMessage(R.string.PassphrasePromptActivity_help_prompt_body) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - SignalStore.misc().setLockScreenAttemptCount(0); - if (onDismissed != null) { - onDismissed.run(); - } - }) - .show(); - } - private class PassphraseActionListener implements TextView.OnEditorActionListener { @Override public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) { @@ -403,13 +353,31 @@ public class PassphrasePromptActivity extends PassphraseActivity { System.gc(); } + private void showHelpDialog() { + lockScreenButton.setText(R.string.prompt_passphrase_activity__try_again); + + learnMoreText.setVisibility(View.VISIBLE); + learnMoreText.setLearnMoreVisible(true); + learnMoreText.setLinkColor(ContextCompat.getColor(PassphrasePromptActivity.this, R.color.signal_colorPrimary)); + + learnMoreText.setOnClickListener(v -> + new MaterialAlertDialogBuilder(PassphrasePromptActivity.this) + .setTitle(R.string.prompt_passphrase_activity__unlock_signal) + .setMessage(R.string.prompt_passphrase_activity__screen_lock_is_on) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(R.string.prompt_passphrase_activity__contact_support, (d,w) -> sendEmailToSupport()) + .show() + ); + } + private class BiometricAuthenticationListener extends BiometricPrompt.AuthenticationCallback { @Override public void onAuthenticationError(int errorCode, @NonNull CharSequence errorString) { Log.w(TAG, "Authentication error: " + errorCode); hadFailure = true; - incrementAttemptCountAndShowHelpIfNecessary(); + showHelpDialog(); if (errorCode != BiometricPrompt.ERROR_CANCELED && errorCode != BiometricPrompt.ERROR_USER_CANCELED) { onAuthenticationFailed(); @@ -419,41 +387,19 @@ public class PassphrasePromptActivity extends PassphraseActivity { @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { Log.i(TAG, "onAuthenticationSucceeded"); - fingerprintPrompt.setImageResource(R.drawable.symbol_check_white_48); - fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN); - fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() { + unlockView.addAnimatorListener(new AnimationCompleteListener() { @Override public void onAnimationEnd(Animator animation) { handleAuthenticated(); } - }).start(); + }); + unlockView.playAnimation(); } @Override public void onAuthenticationFailed() { Log.w(TAG, "onAuthenticationFailed()"); - - fingerprintPrompt.setImageResource(R.drawable.symbol_x_white_48); - fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN); - - TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0); - shake.setDuration(50); - shake.setRepeatCount(7); - shake.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) {} - - @Override - public void onAnimationEnd(Animation animation) { - fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp); - fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN); - } - - @Override - public void onAnimationRepeat(Animation animation) {} - }); - - fingerprintPrompt.startAnimation(shake); + showHelpDialog(); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java index 8b70e79d2e..df0d57cdb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java @@ -287,8 +287,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements this.clearKeyReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + TextSecurePreferences.isPasswordDisabled(context) + ", ScreenLock: " + TextSecurePreferences.isScreenLockEnabled(context)); - if (TextSecurePreferences.isScreenLockEnabled(context) || !TextSecurePreferences.isPasswordDisabled(context)) { + Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + SignalStore.settings().getPassphraseDisabled() + ", ScreenLock: " + SignalStore.settings().getScreenLockEnabled()); + if (SignalStore.settings().getScreenLockEnabled() || !SignalStore.settings().getPassphraseDisabled()) { onMasterSecretCleared(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt index 6ac3f93d89..9e54cc639e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt @@ -109,8 +109,13 @@ class LearnMoreTextPreferenceViewHolder(itemView: View) : PreferenceViewHolder(itemView) { override fun bind(model: ClickPreference) { super.bind(model) - itemView.setOnClickListener { model.onClick() } - itemView.setOnLongClickListener { model.onLongClick?.invoke() ?: false } + if (!itemView.isEnabled && model.onDisabledClicked != null) { + itemView.isEnabled = true + itemView.setOnClickListener { model.onDisabledClicked() } + } else { + itemView.setOnClickListener { model.onClick() } + itemView.setOnLongClickListener { model.onLongClick?.invoke() ?: false } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt index 3aba9d95c4..14b1477b82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt @@ -21,6 +21,7 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.navArgs import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.BiometricDeviceAuthentication import org.thoughtcrime.securesms.BiometricDeviceLockContract @@ -36,9 +37,9 @@ import org.thoughtcrime.securesms.components.settings.PreferenceModel import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.crypto.MasterSecretUtil +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.CommunicationActions -import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ExpirationUtil import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.SpanUtil @@ -197,7 +198,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac KeyCachingService.getMasterSecret(context), MasterSecretUtil.UNENCRYPTED_PASSPHRASE ) - TextSecurePreferences.setPasswordDisabled(activity, true) + SignalStore.settings.passphraseDisabled = true val intent = Intent(activity, KeyCachingService::class.java) intent.action = KeyCachingService.DISABLE_ACTION requireActivity().startService(intent) @@ -249,33 +250,21 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac } else { val isKeyguardSecure = ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure - switchPref( - title = DSLSettingsText.from(R.string.preferences_app_protection__screen_lock), - summary = DSLSettingsText.from(R.string.preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint), - isChecked = state.screenLock && isKeyguardSecure, - isEnabled = isKeyguardSecure, - onClick = { - viewModel.setScreenLockEnabled(!state.screenLock) - - val intent = Intent(requireContext(), KeyCachingService::class.java) - intent.action = KeyCachingService.LOCK_TOGGLED_EVENT - requireContext().startService(intent) - - ConversationUtil.refreshRecipientShortcuts() - } - ) - clickPref( - title = DSLSettingsText.from(R.string.preferences_app_protection__screen_lock_inactivity_timeout), - summary = DSLSettingsText.from(getScreenLockInactivityTimeoutSummary(state.screenLockActivityTimeout)), - isEnabled = isKeyguardSecure && state.screenLock, + title = DSLSettingsText.from(R.string.preferences_app_protection__screen_lock), + summary = DSLSettingsText.from(getScreenLockInactivityTimeoutSummary(isKeyguardSecure && state.screenLock, state.screenLockActivityTimeout)), onClick = { - childFragmentManager.clearFragmentResult(TimeDurationPickerDialog.RESULT_DURATION) - childFragmentManager.clearFragmentResultListener(TimeDurationPickerDialog.RESULT_DURATION) - childFragmentManager.setFragmentResultListener(TimeDurationPickerDialog.RESULT_DURATION, this@PrivacySettingsFragment) { _, bundle -> - viewModel.setScreenLockTimeout(bundle.getLong(TimeDurationPickerDialog.RESULT_KEY_DURATION_MILLISECONDS).milliseconds.inWholeSeconds) - } - TimeDurationPickerDialog.create(state.screenLockActivityTimeout.seconds).show(childFragmentManager, null) + Navigation.findNavController(requireView()).safeNavigate(R.id.action_privacySettingsFragment_to_screenLockSettingsFragment) + }, + isEnabled = isKeyguardSecure, + onDisabledClicked = { + Snackbar + .make( + requireView(), + resources.getString(R.string.preferences_app_protection__to_use_screen_lock), + Snackbar.LENGTH_LONG + ) + .show() } ) } @@ -362,9 +351,11 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac } } - private fun getScreenLockInactivityTimeoutSummary(timeoutSeconds: Long): String { - return if (timeoutSeconds <= 0) { - getString(R.string.AppProtectionPreferenceFragment_none) + private fun getScreenLockInactivityTimeoutSummary(enabledScreenLock: Boolean, timeoutSeconds: Long): String { + return if (!enabledScreenLock) { + getString(R.string.ScreenLockSettingsFragment__off) + } else if (timeoutSeconds == 0L) { + getString(R.string.ScreenLockSettingsFragment__immediately) } else { ExpirationUtil.getExpirationDisplayValue(requireContext(), timeoutSeconds.toInt()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt index 56b1eafa0d..408c988a4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsViewModel.kt @@ -37,16 +37,6 @@ class PrivacySettingsViewModel( refresh() } - fun setScreenLockEnabled(enabled: Boolean) { - sharedPreferences.edit().putBoolean(TextSecurePreferences.SCREEN_LOCK, enabled).apply() - refresh() - } - - fun setScreenLockTimeout(seconds: Long) { - TextSecurePreferences.setScreenLockTimeout(AppDependencies.application, seconds) - refresh() - } - fun setScreenSecurityEnabled(enabled: Boolean) { sharedPreferences.edit().putBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, enabled).apply() refresh() @@ -63,12 +53,12 @@ class PrivacySettingsViewModel( } fun setObsoletePasswordTimeoutEnabled(enabled: Boolean) { - sharedPreferences.edit().putBoolean(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF, enabled).apply() + SignalStore.settings.passphraseTimeoutEnabled = enabled refresh() } fun setObsoletePasswordTimeout(minutes: Int) { - TextSecurePreferences.setPassphraseTimeoutInterval(AppDependencies.application, minutes) + SignalStore.settings.passphraseTimeout = minutes refresh() } @@ -81,14 +71,14 @@ class PrivacySettingsViewModel( blockedCount = 0, readReceipts = TextSecurePreferences.isReadReceiptsEnabled(AppDependencies.application), typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(AppDependencies.application), - screenLock = TextSecurePreferences.isScreenLockEnabled(AppDependencies.application), - screenLockActivityTimeout = TextSecurePreferences.getScreenLockTimeout(AppDependencies.application), + screenLock = SignalStore.settings.screenLockEnabled, + screenLockActivityTimeout = SignalStore.settings.screenLockTimeout, screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(AppDependencies.application), incognitoKeyboard = TextSecurePreferences.isIncognitoKeyboardEnabled(AppDependencies.application), paymentLock = SignalStore.payments.paymentLock, - isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(AppDependencies.application), - isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(AppDependencies.application), - obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(AppDependencies.application), + isObsoletePasswordEnabled = !SignalStore.settings.passphraseDisabled, + isObsoletePasswordTimeoutEnabled = SignalStore.settings.passphraseTimeoutEnabled, + obsoletePasswordTimeout = SignalStore.settings.passphraseTimeout, universalExpireTimer = SignalStore.settings.universalExpireTimer ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt index c5b87cb13d..2e26dd7e1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/expire/CustomExpireTimerSelectorView.kt @@ -65,6 +65,12 @@ class CustomExpireTimerSelectorView @JvmOverloads constructor( valuePicker.maxValue = timerUnit.maxValue } + fun setUnits(minValue: Int, maxValue: Int, timeUnitRes: Int) { + unitPicker.minValue = minValue + unitPicker.maxValue = maxValue + unitPicker.displayedValues = context.resources.getStringArray(timeUnitRes) + } + private enum class TimerUnit(val minValue: Int, val maxValue: Int, val valueMultiplier: Long) { SECONDS(1, 59, TimeUnit.SECONDS.toSeconds(1)), MINUTES(1, 59, TimeUnit.MINUTES.toSeconds(1)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/CustomScreenLockTimerSelectDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/CustomScreenLockTimerSelectDialog.kt new file mode 100644 index 0000000000..1fbdb9cf86 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/CustomScreenLockTimerSelectDialog.kt @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.components.settings.app.privacy.screenlock + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.privacy.expire.CustomExpireTimerSelectorView + +/** + * Dialog for selecting a custom timer value when setting the screen lock timeout. + */ +class CustomScreenLockTimerSelectDialog : DialogFragment() { + + private val viewModel: ScreenLockSettingsViewModel by activityViewModels() + private lateinit var selector: CustomExpireTimerSelectorView + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialogView: View = LayoutInflater.from(context).inflate(R.layout.custom_expire_timer_select_dialog, null, false) + + selector = dialogView.findViewById(R.id.custom_expire_timer_select_dialog_selector) + selector.setUnits(1, 3, R.array.CustomScreenLockTimerSelectorView__unit_labels) + selector.setTimer(viewModel.state.value.screenLockActivityTimeout.toInt()) + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.ExpireTimerSettingsFragment__custom_time) + .setView(dialogView) + .setPositiveButton(R.string.ExpireTimerSettingsFragment__set) { _, _ -> + viewModel.setScreenLockTimeout(selector.getTimer().toLong()) + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsFragment.kt new file mode 100644 index 0000000000..ff7f8fe60c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsFragment.kt @@ -0,0 +1,284 @@ +package org.thoughtcrime.securesms.components.settings.app.privacy.screenlock + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.activity.result.ActivityResultLauncher +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import org.signal.core.ui.Previews +import org.signal.core.ui.Scaffolds +import org.signal.core.ui.SignalPreview +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BiometricDeviceAuthentication +import org.thoughtcrime.securesms.BiometricDeviceLockContract +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeFragment +import org.thoughtcrime.securesms.service.KeyCachingService +import org.thoughtcrime.securesms.util.ConversationUtil +import org.thoughtcrime.securesms.util.ExpirationUtil +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Fragment that allows user to turn on screen lock and set a timer to lock + */ +class ScreenLockSettingsFragment : ComposeFragment() { + + companion object { + private val TAG = Log.tag(ScreenLockSettingsFragment::class) + } + + private val viewModel: ScreenLockSettingsViewModel by activityViewModels() + + private lateinit var biometricAuth: BiometricDeviceAuthentication + private lateinit var biometricDeviceLockLauncher: ActivityResultLauncher + private lateinit var disableLockPromptInfo: BiometricPrompt.PromptInfo + private lateinit var enableLockPromptInfo: BiometricPrompt.PromptInfo + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + biometricDeviceLockLauncher = registerForActivityResult(BiometricDeviceLockContract()) { result: Int -> + if (result == BiometricDeviceAuthentication.AUTHENTICATED) { + toggleScreenLock() + } + } + + enableLockPromptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) + .setTitle(requireContext().getString(R.string.ScreenLockSettingsFragment__use_signal_screen_lock)) + .setConfirmationRequired(true) + .build() + + disableLockPromptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS) + .setTitle(requireContext().getString(R.string.ScreenLockSettingsFragment__turn_off_signal_lock)) + .setConfirmationRequired(true) + .build() + + biometricAuth = BiometricDeviceAuthentication( + BiometricManager.from(requireActivity()), + BiometricPrompt(requireActivity(), BiometricAuthenticationListener()), + enableLockPromptInfo + ) + } + + override fun onPause() { + super.onPause() + biometricAuth.cancelAuthentication() + } + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsState() + val navController: NavController by remember { mutableStateOf(findNavController()) } + + Scaffolds.Settings( + title = stringResource(id = R.string.preferences_app_protection__screen_lock), + onNavigationClick = { navController.popBackStack() }, + navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24), + navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close) + ) { contentPadding: PaddingValues -> + ScreenLockScreen( + state = state, + onChecked = { checked -> + if (biometricAuth.canAuthenticate() && !checked) { + biometricAuth.updatePromptInfo(disableLockPromptInfo) + biometricAuth.authenticate(requireContext(), true) { + biometricDeviceLockLauncher.launch(getString(R.string.ScreenLockSettingsFragment__turn_off_signal_lock)) + } + } else if (biometricAuth.canAuthenticate() && checked) { + biometricAuth.updatePromptInfo(enableLockPromptInfo) + biometricAuth.authenticate(requireContext(), true) { + biometricDeviceLockLauncher.launch(getString(R.string.ScreenLockSettingsFragment__use_screen_lock)) + } + } + }, + onTimeClicked = viewModel::setScreenLockTimeout, + onCustomTimeClicked = { navController.safeNavigate(R.id.action_screenLockSettingsFragment_to_customScreenLockTimerSelectDialog) }, + modifier = Modifier.padding(contentPadding) + ) + } + } + + private fun toggleScreenLock() { + viewModel.toggleScreenLock() + + val intent = Intent(requireContext(), KeyCachingService::class.java) + intent.action = KeyCachingService.LOCK_TOGGLED_EVENT + requireContext().startService(intent) + + ConversationUtil.refreshRecipientShortcuts() + } + + private inner class BiometricAuthenticationListener : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errorString: CharSequence) { + Log.w(TAG, "Authentication error: $errorCode") + onAuthenticationFailed() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + Log.i(TAG, "Authentication succeeded") + toggleScreenLock() + } + + override fun onAuthenticationFailed() { + Log.w(TAG, "Unable to authenticate") + } + } +} + +@Composable +fun ScreenLockScreen( + state: ScreenLockSettingsState, + onChecked: (Boolean) -> Unit, + onTimeClicked: (Long) -> Unit, + onCustomTimeClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier.verticalScroll(rememberScrollState())) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = painterResource(R.drawable.ic_screen_lock), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.padding(top = 24.dp, bottom = 24.dp) + ) + Text( + text = stringResource(id = R.string.ScreenLockSettingsFragment__your_android_device), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 40.dp, end = 40.dp, bottom = 24.dp) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 24.dp) + .background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(24.dp)) + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 12.dp) + ) { + Text(stringResource(id = R.string.ScreenLockSettingsFragment__use_screen_lock)) + Spacer(Modifier.weight(1f)) + Switch(checked = state.screenLock, onCheckedChange = onChecked) + } + } + + if (state.screenLock) { + val labels: List = LocalContext.current.resources.getStringArray(R.array.ScreenLockSettingsFragment__labels).toList() + val values: List = LocalContext.current.resources.getIntArray(R.array.ScreenLockSettingsFragment__values).map { it.toLong() } + + Text( + stringResource(id = R.string.ScreenLockSettingsFragment__start_screen_lock), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.padding(top = 24.dp, bottom = 16.dp, start = 24.dp) + ) + Column(Modifier.selectableGroup()) { + var isCustomTime = true + labels.zip(values).forEach { (label, seconds) -> + val isSelected = seconds == state.screenLockActivityTimeout + Row( + Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .selectable( + selected = isSelected, + onClick = { onTimeClicked(seconds) }, + role = Role.RadioButton + ) + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton(selected = isSelected, onClick = null) + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 16.dp) + ) + isCustomTime = isCustomTime && !isSelected + } + } + + Row( + Modifier + .fillMaxWidth() + .height(56.dp) + .selectable( + selected = isCustomTime, + onClick = onCustomTimeClicked, + role = Role.RadioButton + ) + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton(selected = isCustomTime, onClick = null) + Column(modifier = Modifier.padding(start = 16.dp)) { + Text( + text = stringResource(id = R.string.ScreenLockSettingsFragment__custom_time), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + if (isCustomTime && state.screenLockActivityTimeout > 0) { + Text( + text = ExpirationUtil.getExpirationDisplayValue(LocalContext.current, state.screenLockActivityTimeout.toInt()), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + } +} + +@SignalPreview +@Composable +fun ScreenLockScreenPreview() { + Previews.Preview { + ScreenLockScreen( + state = ScreenLockSettingsState(true, 60), + onChecked = {}, + onTimeClicked = {}, + onCustomTimeClicked = {} + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsState.kt new file mode 100644 index 0000000000..0d2fb1dcb5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsState.kt @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.components.settings.app.privacy.screenlock + +/** + * Information about the screen lock state. Used in [ScreenLockSettingsViewModel]. + */ +data class ScreenLockSettingsState( + val screenLock: Boolean = false, + val screenLockActivityTimeout: Long = 0L +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsViewModel.kt new file mode 100644 index 0000000000..25baa4c5a4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/screenlock/ScreenLockSettingsViewModel.kt @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.components.settings.app.privacy.screenlock + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.thoughtcrime.securesms.keyvalue.SignalStore + +/** + * Maintains the state of the [ScreenLockSettingsFragment] + */ +class ScreenLockSettingsViewModel : ViewModel() { + + private val _state = MutableStateFlow(getState()) + val state = _state.asStateFlow() + + fun toggleScreenLock() { + val enabled = !_state.value.screenLock + SignalStore.settings.screenLockEnabled = enabled + _state.update { + it.copy( + screenLock = enabled + ) + } + } + + fun setScreenLockTimeout(seconds: Long) { + SignalStore.settings.screenLockTimeout = seconds + _state.update { + it.copy( + screenLockActivityTimeout = seconds + ) + } + } + + private fun getState(): ScreenLockSettingsState { + return ScreenLockSettingsState( + screenLock = SignalStore.settings.screenLockEnabled, + screenLockActivityTimeout = SignalStore.settings.screenLockTimeout + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt index a1aae2cc22..8a3acd0dbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt @@ -105,9 +105,10 @@ class DSLConfiguration { iconEnd: DSLSettingsIcon? = null, isEnabled: Boolean = true, onClick: () -> Unit, - onLongClick: (() -> Boolean)? = null + onLongClick: (() -> Boolean)? = null, + onDisabledClicked: () -> Unit = {} ) { - val preference = ClickPreference(title, summary, icon, iconEnd, isEnabled, onClick, onLongClick) + val preference = ClickPreference(title, summary, icon, iconEnd, isEnabled, onClick, onLongClick, onDisabledClicked) children.add(preference) } @@ -344,7 +345,8 @@ class ClickPreference( override val iconEnd: DSLSettingsIcon? = null, override val isEnabled: Boolean = true, val onClick: () -> Unit, - val onLongClick: (() -> Boolean)? = null + val onLongClick: (() -> Boolean)? = null, + val onDisabledClicked: () -> Unit = {} ) : PreferenceModel() class LongClickPreference( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index e8d2585ead..bd41e9a55d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -568,7 +568,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public void onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.menu_clear_passphrase).setVisible(!TextSecurePreferences.isPasswordDisabled(requireContext())); + menu.findItem(R.id.menu_clear_passphrase).setVisible(!SignalStore.settings().getPassphraseDisabled()); ConversationFilterRequest request = viewModel.getConversationFilterRequest(); boolean isChatFilterEnabled = request != null && request.getFilter() == ConversationFilter.UNREAD; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt index 3a34fd44be..680554f396 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutRankingUpdateJob.kt @@ -5,12 +5,12 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JsonJobData +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.transport.RetryLaterException import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ConversationUtil.Direction -import org.thoughtcrime.securesms.util.TextSecurePreferences import kotlin.time.Duration.Companion.seconds /** @@ -65,7 +65,7 @@ class ConversationShortcutRankingUpdateJob private constructor( override fun getFactoryKey() = KEY override fun onRun() { - if (TextSecurePreferences.isScreenLockEnabled(context)) { + if (SignalStore.settings.screenLockEnabled) { Log.i(TAG, "Screen lock enabled. Clearing shortcuts.") ConversationUtil.clearAllShortcuts(context) return diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java index 6d5f45b23b..ea033062aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ConversationShortcutUpdateJob.java @@ -9,10 +9,10 @@ import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.ConversationUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.util.ArrayList; import java.util.List; @@ -58,7 +58,7 @@ public class ConversationShortcutUpdateJob extends BaseJob { @Override protected void onRun() throws Exception { - if (TextSecurePreferences.isScreenLockEnabled(context)) { + if (SignalStore.settings().getScreenLockEnabled()) { Log.i(TAG, "Screen lock enabled. Clearing shortcuts."); ConversationUtil.clearAllShortcuts(context); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java index 018fa82b27..c005714f38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -67,6 +67,11 @@ public final class SettingsValues extends SignalStoreValues { private static final String KEEP_MUTED_CHATS_ARCHIVED = "settings.keepMutedChatsArchived"; private static final String USE_COMPACT_NAVIGATION_BAR = "settings.useCompactNavigationBar"; private static final String THREAD_TRIM_SYNC_TO_LINKED_DEVICES = "settings.storage.syncThreadTrimDeletes"; + private static final String PASSPHRASE_DISABLED = "settings.passphrase.disabled"; + private static final String PASSPHRASE_TIMEOUT_ENABLED = "settings.passphrase.timeout.enabled"; + private static final String PASSPHRASE_TIMEOUT = "settings.passphrase.timeout"; + private static final String SCREEN_LOCK_ENABLED = "settings.screen.lock.enabled"; + private static final String SCREEN_LOCK_TIMEOUT = "settings.screen.lock.timeout"; public static final int BACKUP_DEFAULT_HOUR = 2; public static final int BACKUP_DEFAULT_MINUTE = 0; @@ -75,6 +80,20 @@ public final class SettingsValues extends SignalStoreValues { SettingsValues(@NonNull KeyValueStore store) { super(store); + + if (!store.containsKey(SCREEN_LOCK_ENABLED)) { + migrateFromSharedPrefsV1(AppDependencies.getApplication()); + } + } + + private void migrateFromSharedPrefsV1(@NonNull Context context) { + Log.i(TAG, "[V1] Migrating screen lock values from shared prefs."); + + putBoolean(PASSPHRASE_DISABLED, TextSecurePreferences.getBooleanPreference(context, "pref_disable_passphrase", true)); + putBoolean(PASSPHRASE_TIMEOUT_ENABLED, TextSecurePreferences.getBooleanPreference(context, "pref_timeout_passphrase", false)); + putInteger(PASSPHRASE_TIMEOUT, TextSecurePreferences.getIntegerPreference(context, "pref_timeout_interval", 5 * 60)); + putBoolean(SCREEN_LOCK_ENABLED, TextSecurePreferences.getBooleanPreference(context, "pref_android_screen_lock", false)); + putLong(SCREEN_LOCK_TIMEOUT, TextSecurePreferences.getLongPreference(context, "pref_android_screen_lock_timeout", 0)); } @Override @@ -121,7 +140,12 @@ public final class SettingsValues extends SignalStoreValues { SENT_MEDIA_QUALITY, KEEP_MUTED_CHATS_ARCHIVED, USE_COMPACT_NAVIGATION_BAR, - THREAD_TRIM_SYNC_TO_LINKED_DEVICES); + THREAD_TRIM_SYNC_TO_LINKED_DEVICES, + PASSPHRASE_DISABLED, + PASSPHRASE_TIMEOUT_ENABLED, + PASSPHRASE_TIMEOUT, + SCREEN_LOCK_ENABLED, + SCREEN_LOCK_TIMEOUT); } public @NonNull LiveData getOnConfigurationSettingChanged() { @@ -461,6 +485,46 @@ public final class SettingsValues extends SignalStoreValues { return getBoolean(USE_COMPACT_NAVIGATION_BAR, false); } + public void setPassphraseDisabled(boolean disabled) { + putBoolean(PASSPHRASE_DISABLED, disabled); + } + + public boolean getPassphraseDisabled() { + return getBoolean(PASSPHRASE_DISABLED, true); + } + + public void setPassphraseTimeoutEnabled(boolean enabled) { + putBoolean(PASSPHRASE_TIMEOUT_ENABLED, enabled); + } + + public boolean getPassphraseTimeoutEnabled() { + return getBoolean(PASSPHRASE_TIMEOUT_ENABLED, false); + } + + public void setPassphraseTimeout(int minutes) { + putLong(PASSPHRASE_TIMEOUT, minutes); + } + + public int getPassphraseTimeout() { + return getInteger(PASSPHRASE_TIMEOUT, 0); + } + + public void setScreenLockEnabled(boolean enabled) { + putBoolean(SCREEN_LOCK_ENABLED, enabled); + } + + public boolean getScreenLockEnabled() { + return getBoolean(SCREEN_LOCK_ENABLED, false); + } + + public void setScreenLockTimeout(long seconds) { + putLong(SCREEN_LOCK_TIMEOUT, seconds); + } + + public long getScreenLockTimeout() { + return getLong(SCREEN_LOCK_TIMEOUT, 0); + } + private @Nullable Uri getUri(@NonNull String key) { String uri = getString(key, ""); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionKeyPreferences.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionKeyPreferences.java index aea02a3345..c43c5e9940 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionKeyPreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionKeyPreferences.java @@ -19,9 +19,9 @@ final class LogSectionKeyPreferences implements LogSection { @Override public @NonNull CharSequence getContent(@NonNull Context context) { - return new StringBuilder().append("Screen Lock : ").append(TextSecurePreferences.isScreenLockEnabled(context)).append("\n") - .append("Screen Lock Timeout : ").append(TextSecurePreferences.getScreenLockTimeout(context)).append("\n") - .append("Password Disabled : ").append(TextSecurePreferences.isPasswordDisabled(context)).append("\n") + return new StringBuilder().append("Screen Lock : ").append(SignalStore.settings().getScreenLockEnabled()).append("\n") + .append("Screen Lock Timeout : ").append(SignalStore.settings().getScreenLockTimeout()).append("\n") + .append("Password Disabled : ").append(SignalStore.settings().getPassphraseDisabled()).append("\n") .append("Prefer Contact Photos : ").append(SignalStore.settings().isPreferSystemContactPhotos()).append("\n") .append("Call Data Mode : ").append(SignalStore.settings().getCallDataMode()).append("\n") .append("Media Quality : ").append(SignalStore.settings().getSentMediaQuality()).append("\n") diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java index 5d23ff3668..54938c5f87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -32,6 +32,7 @@ import android.os.SystemClock; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BuildConfig; @@ -42,11 +43,11 @@ import org.thoughtcrime.securesms.crypto.InvalidPassphraseException; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.dependencies.AppDependencies; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.ServiceUtil; -import org.thoughtcrime.securesms.util.TextSecurePreferences; import java.util.concurrent.TimeUnit; @@ -80,17 +81,17 @@ public class KeyCachingService extends Service { public KeyCachingService() {} public static synchronized boolean isLocked(Context context) { - boolean locked = masterSecret == null && (!TextSecurePreferences.isPasswordDisabled(context) || TextSecurePreferences.isScreenLockEnabled(context)); + boolean locked = masterSecret == null && (!SignalStore.settings().getPassphraseDisabled() || SignalStore.settings().getScreenLockEnabled()); if (locked) { - Log.d(TAG, "Locked! PasswordDisabled: " + TextSecurePreferences.isPasswordDisabled(context) + ", ScreenLock: " + TextSecurePreferences.isScreenLockEnabled(context)); + Log.d(TAG, "Locked! PasswordDisabled: " + SignalStore.settings().getPassphraseDisabled() + ", ScreenLock: " + SignalStore.settings().getScreenLockEnabled()); } return locked; } public static synchronized @Nullable MasterSecret getMasterSecret(Context context) { - if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) { + if (masterSecret == null && (SignalStore.settings().getPassphraseDisabled() && !SignalStore.settings().getScreenLockEnabled())) { try { return MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); } catch (InvalidPassphraseException e) { @@ -102,7 +103,7 @@ public class KeyCachingService extends Service { } public static void onAppForegrounded(@NonNull Context context) { - if (TextSecurePreferences.isScreenLockEnabled(context) || !TextSecurePreferences.isPasswordDisabled(context)) { + if (SignalStore.settings().getScreenLockEnabled() || !SignalStore.settings().getPassphraseDisabled()) { ServiceUtil.getAlarmManager(context).cancel(buildExpirationPendingIntent(context)); } } @@ -155,7 +156,7 @@ public class KeyCachingService extends Service { Log.i(TAG, "onCreate()"); super.onCreate(); - if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) { + if (SignalStore.settings().getPassphraseDisabled() && !SignalStore.settings().getScreenLockEnabled()) { try { MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); setMasterSecret(masterSecret); @@ -215,8 +216,8 @@ public class KeyCachingService extends Service { } private void handleDisableService() { - if (TextSecurePreferences.isPasswordDisabled(this) && - !TextSecurePreferences.isScreenLockEnabled(this)) + if (SignalStore.settings().getPassphraseDisabled() && + !SignalStore.settings().getScreenLockEnabled()) { stopForeground(true); } @@ -231,33 +232,40 @@ public class KeyCachingService extends Service { boolean appVisible = AppDependencies.getAppForegroundObserver().isForegrounded(); boolean secretSet = KeyCachingService.masterSecret != null; - boolean timeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(context); - boolean passLockActive = timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(context); + boolean timeoutEnabled = SignalStore.settings().getPassphraseTimeoutEnabled(); + boolean passLockActive = timeoutEnabled && !SignalStore.settings().getPassphraseDisabled(); - long screenTimeout = TextSecurePreferences.getScreenLockTimeout(context); - boolean screenLockActive = screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(context); + long screenTimeout = SignalStore.settings().getScreenLockTimeout(); + boolean screenLockActive = SignalStore.settings().getScreenLockEnabled(); + boolean immediateScreenLock = screenTimeout == 0 && screenLockActive; if (!appVisible && secretSet && (passLockActive || screenLockActive)) { - long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(context); - long screenLockTimeoutSeconds = TextSecurePreferences.getScreenLockTimeout(context); + if (immediateScreenLock) { + Log.i(TAG, "Starting immediate screen lock"); + Intent intent = new Intent(PASSPHRASE_EXPIRED_EVENT, null, context, KeyCachingService.class); + context.startService(intent); + } else { + long passphraseTimeoutMinutes = SignalStore.settings().getPassphraseTimeout(); + long screenLockTimeoutSeconds = SignalStore.settings().getScreenLockTimeout(); - long timeoutMillis; + long timeoutMillis; - if (!TextSecurePreferences.isPasswordDisabled(context)) timeoutMillis = TimeUnit.MINUTES.toMillis(passphraseTimeoutMinutes); - else timeoutMillis = TimeUnit.SECONDS.toMillis(screenLockTimeoutSeconds); + if (!SignalStore.settings().getPassphraseDisabled()) timeoutMillis = TimeUnit.MINUTES.toMillis(passphraseTimeoutMinutes); + else timeoutMillis = TimeUnit.SECONDS.toMillis(screenLockTimeoutSeconds); - Log.i(TAG, "Starting timeout: " + timeoutMillis); + Log.i(TAG, "Starting timeout: " + timeoutMillis); - AlarmManager alarmManager = ServiceUtil.getAlarmManager(context); - PendingIntent expirationIntent = buildExpirationPendingIntent(context); + AlarmManager alarmManager = ServiceUtil.getAlarmManager(context); + PendingIntent expirationIntent = buildExpirationPendingIntent(context); - alarmManager.cancel(expirationIntent); - alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, expirationIntent); + alarmManager.cancel(expirationIntent); + alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeoutMillis, expirationIntent); + } } } private void foregroundService() { - if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) { + if (SignalStore.settings().getPassphraseDisabled() && !SignalStore.settings().getScreenLockEnabled()) { stopForeground(true); return; } @@ -267,7 +275,8 @@ public class KeyCachingService extends Service { builder.setContentTitle(getString(R.string.KeyCachingService_passphrase_cached)); builder.setContentText(getString(R.string.KeyCachingService_signal_passphrase_cached)); - builder.setSmallIcon(R.drawable.icon_cached); + builder.setSmallIcon(R.drawable.ic_notification_unlocked); + builder.setColor(ContextCompat.getColor(this, R.color.signal_light_colorSecondary)); builder.setWhen(0); builder.setPriority(Notification.PRIORITY_MIN); builder.setOngoing(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.kt b/app/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.kt index 9698990e41..f50f21c029 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/PanicResponderListener.kt @@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.ServiceUtil -import org.thoughtcrime.securesms.util.TextSecurePreferences /** * Respond to a PanicKit trigger Intent by locking the app. PanicKit provides a @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences */ class PanicResponderListener : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - val passwordEnabled = !TextSecurePreferences.isPasswordDisabled(context) + val passwordEnabled = !SignalStore.settings.passphraseDisabled val keyguardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure val intentAction = intent.action if ((passwordEnabled || keyguardSecure) && "info.guardianproject.panic.action.TRIGGER" == intentAction) { diff --git a/app/src/main/res/drawable-hdpi/ic_notification_unlocked.png b/app/src/main/res/drawable-hdpi/ic_notification_unlocked.png new file mode 100644 index 0000000000..a4bad75b85 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification_unlocked.png differ diff --git a/app/src/main/res/drawable-hdpi/icon_cached.webp b/app/src/main/res/drawable-hdpi/icon_cached.webp deleted file mode 100644 index 3c0cb94554..0000000000 Binary files a/app/src/main/res/drawable-hdpi/icon_cached.webp and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_unlocked.png b/app/src/main/res/drawable-mdpi/ic_notification_unlocked.png new file mode 100644 index 0000000000..10220a3802 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification_unlocked.png differ diff --git a/app/src/main/res/drawable-mdpi/icon_cached.webp b/app/src/main/res/drawable-mdpi/icon_cached.webp deleted file mode 100644 index 823555c4d4..0000000000 Binary files a/app/src/main/res/drawable-mdpi/icon_cached.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification_unlocked.png b/app/src/main/res/drawable-xhdpi/ic_notification_unlocked.png new file mode 100644 index 0000000000..b0ae0478cb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification_unlocked.png differ diff --git a/app/src/main/res/drawable-xhdpi/icon_cached.webp b/app/src/main/res/drawable-xhdpi/icon_cached.webp deleted file mode 100644 index 1adb3f6d03..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/icon_cached.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_unlocked.png b/app/src/main/res/drawable-xxhdpi/ic_notification_unlocked.png new file mode 100644 index 0000000000..a7d0560df8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification_unlocked.png differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_cached.webp b/app/src/main/res/drawable-xxhdpi/icon_cached.webp deleted file mode 100644 index f04bb608e7..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_cached.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification_unlocked.png b/app/src/main/res/drawable-xxxhdpi/ic_notification_unlocked.png new file mode 100644 index 0000000000..d794d08aff Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification_unlocked.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_cached.webp b/app/src/main/res/drawable-xxxhdpi/icon_cached.webp deleted file mode 100644 index 44e78ed43f..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_cached.webp and /dev/null differ diff --git a/app/src/main/res/drawable/ic_screen_lock.xml b/app/src/main/res/drawable/ic_screen_lock.xml new file mode 100644 index 0000000000..27008ac32d --- /dev/null +++ b/app/src/main/res/drawable/ic_screen_lock.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/prompt_passphrase_activity.xml b/app/src/main/res/layout/prompt_passphrase_activity.xml index 863c42c1ab..a4d6788766 100644 --- a/app/src/main/res/layout/prompt_passphrase_activity.xml +++ b/app/src/main/res/layout/prompt_passphrase_activity.xml @@ -1,8 +1,8 @@ - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" + android:theme="@style/TextSecure.LightActionBar.DarkText" + android:layout_marginTop="20dp" /> + android:gravity="center"> - + - - - + - \ No newline at end of file + + + + + + diff --git a/app/src/main/res/menu/passphrase_prompt.xml b/app/src/main/res/menu/passphrase_prompt.xml index a3ee2a9b9a..abd321d48d 100644 --- a/app/src/main/res/menu/passphrase_prompt.xml +++ b/app/src/main/res/menu/passphrase_prompt.xml @@ -4,7 +4,4 @@ - - \ No newline at end of file diff --git a/app/src/main/res/navigation/privacy_settings.xml b/app/src/main/res/navigation/privacy_settings.xml index b0448cd860..07f27429b5 100644 --- a/app/src/main/res/navigation/privacy_settings.xml +++ b/app/src/main/res/navigation/privacy_settings.xml @@ -42,6 +42,13 @@ app:exitAnim="@anim/fragment_open_exit" app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" /> + @@ -60,6 +67,19 @@ android:name="org.thoughtcrime.securesms.components.settings.app.privacy.pnp.PhoneNumberPrivacySettingsFragment" android:label="phone_number_privacy_settings_fragment" /> + + + + + + diff --git a/app/src/main/res/raw-night/lottie_unlock.json b/app/src/main/res/raw-night/lottie_unlock.json new file mode 100644 index 0000000000..6635ae667b --- /dev/null +++ b/app/src/main/res/raw-night/lottie_unlock.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.8","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":30.0000012219251,"w":720,"h":720,"nm":"Unlock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":0},"o":{"x":0.2,"y":0.2},"t":0,"s":[280,199,0],"to":[0,0,0],"ti":[0,0,0]},{"t":9.00000036657752,"s":[280,199,0]}]},"a":{"a":0,"k":[220,195,0]},"s":{"a":1,"k":[{"i":{"x":[0.639,0.639,0.639],"y":[0.12,0.911,1]},"o":{"x":[0.532,0.532,0.532],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.596,0.596,0.596],"y":[0.746,0.231,1]},"o":{"x":[0.274,0.274,0.274],"y":[0.187,-0.228,0]},"t":3,"s":[45.06,120.836,100]},{"i":{"x":[0.578,0.578,0.578],"y":[0.791,0.67,1]},"o":{"x":[0.256,0.256,0.256],"y":[0.307,0.267,0]},"t":4,"s":[-20.373,118.773,100]},{"i":{"x":[0.594,0.594,0.594],"y":[0.484,1,1]},"o":{"x":[0.267,0.267,0.267],"y":[0.768,0.559,0]},"t":6,"s":[-89.031,111.228,100]},{"i":{"x":[0.545,0.545,0.545],"y":[1,1,1]},"o":{"x":[0.227,0.227,0.227],"y":[-2.778,0,0]},"t":9,"s":[-106.75,107,100]},{"t":20.0000008146167,"s":[-100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-57.99,0],[0,-57.99],[0,0],[8.284,0],[0,8.284],[0,0],[41.421,0],[0,-41.421],[0,0],[8.284,0],[0,8.284]],"o":[[0,-57.99],[57.99,0],[0,0],[0,8.284],[-8.284,0],[0,0],[0,-41.421],[-41.421,0],[0,0],[0,8.284],[-8.284,0],[0,0]],"v":[[-105,25],[0,-80],[105,25],[105,65],[90,80],[75,65],[75,25],[0,-50],[-75,25],[-75,65],[-90,80],[-105,65]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.886274509804,0.882352941176,0.898039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[130,125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":94.0000038286985,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[350,360,0]},"a":{"a":0,"k":[180,240,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":9,"s":[115,115,100]},{"t":20.0000008146167,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-35.898,0],[0,0],[0,-35.898],[0,0],[35.898,0],[0,0],[0,35.899]],"o":[[0,-35.898],[0,0],[35.898,0],[0,0],[0,35.899],[0,0],[-35.899,0],[0,0]],"v":[[-140,-55],[-75,-120],[75,-120],[140,-55],[140,55],[75,120],[-75,120],[-140,55]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-19.33],[0,0],[-19.33,0],[0,0],[0,19.33],[0,0],[19.33,0]],"o":[[-19.33,0],[0,0],[0,19.33],[0,0],[19.33,0],[0,0],[0,-19.33],[0,0]],"v":[[-75,-90],[-110,-55],[-110,55],[-75,90],[75,90],[110,55],[110,-55],[75,-90]],"c":true}},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0.886274509804,0.882352941176,0.898039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[190,310]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-14.958],[-7.517,-4.813],[0,0],[-8.054,0],[0,8.055],[0,0],[0,9.588],[14.958,0]],"o":[[0,9.588],[0,0],[0,8.055],[8.054,0],[0,0],[7.517,-4.813],[0,-14.958],[-14.957,0]],"v":[[-27.083,-19.792],[-14.583,3.034],[-14.583,32.291],[0,46.875],[14.583,32.291],[14.583,3.034],[27.083,-19.792],[0,-46.875]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.886274509804,0.882352941176,0.898039215686,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[187.083,311.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"k":[{"s":[0],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":900,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":0,"op":900.000036657751,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/raw/lottie_unlock.json b/app/src/main/res/raw/lottie_unlock.json new file mode 100644 index 0000000000..533c39c9bf --- /dev/null +++ b/app/src/main/res/raw/lottie_unlock.json @@ -0,0 +1 @@ +{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.5.8","a":"","k":"","d":"","tc":""},"fr":29.9700012207031,"ip":0,"op":30.0000012219251,"w":720,"h":720,"nm":"Unlock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0,"y":0},"o":{"x":0.2,"y":0.2},"t":0,"s":[280,199,0],"to":[0,0,0],"ti":[0,0,0]},{"t":9.00000036657752,"s":[280,199,0]}]},"a":{"a":0,"k":[220,195,0]},"s":{"a":1,"k":[{"i":{"x":[0.639,0.639,0.639],"y":[0.12,0.911,1]},"o":{"x":[0.532,0.532,0.532],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.596,0.596,0.596],"y":[0.746,0.231,1]},"o":{"x":[0.274,0.274,0.274],"y":[0.187,-0.228,0]},"t":3,"s":[45.06,120.836,100]},{"i":{"x":[0.578,0.578,0.578],"y":[0.791,0.67,1]},"o":{"x":[0.256,0.256,0.256],"y":[0.307,0.267,0]},"t":4,"s":[-20.373,118.773,100]},{"i":{"x":[0.594,0.594,0.594],"y":[0.484,1,1]},"o":{"x":[0.267,0.267,0.267],"y":[0.768,0.559,0]},"t":6,"s":[-89.031,111.228,100]},{"i":{"x":[0.545,0.545,0.545],"y":[1,1,1]},"o":{"x":[0.227,0.227,0.227],"y":[-2.778,0,0]},"t":9,"s":[-106.75,107,100]},{"t":20.0000008146167,"s":[-100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-57.99,0],[0,-57.99],[0,0],[8.284,0],[0,8.284],[0,0],[41.421,0],[0,-41.421],[0,0],[8.284,0],[0,8.284]],"o":[[0,-57.99],[57.99,0],[0,0],[0,8.284],[-8.284,0],[0,0],[0,-41.421],[-41.421,0],[0,0],[0,8.284],[-8.284,0],[0,0]],"v":[[-105,25],[0,-80],[105,25],[105,65],[90,80],[75,65],[75,25],[0,-50],[-75,25],[-75,65],[-90,80],[-105,65]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.105882352941,0.105882352941,0.113725490196,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[130,125]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":94.0000038286985,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[350,360,0]},"a":{"a":0,"k":[180,240,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":9,"s":[115,115,100]},{"t":20.0000008146167,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-35.898,0],[0,0],[0,-35.898],[0,0],[35.898,0],[0,0],[0,35.899]],"o":[[0,-35.898],[0,0],[35.898,0],[0,0],[0,35.899],[0,0],[-35.899,0],[0,0]],"v":[[-140,-55],[-75,-120],[75,-120],[140,-55],[140,55],[75,120],[-75,120],[-140,55]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,-19.33],[0,0],[-19.33,0],[0,0],[0,19.33],[0,0],[19.33,0]],"o":[[-19.33,0],[0,0],[0,19.33],[0,0],[19.33,0],[0,0],[0,-19.33],[0,0]],"v":[[-75,-90],[-110,-55],[-110,55],[-75,90],[75,90],[110,55],[110,-55],[75,-90]],"c":true}},"nm":"Path 2","hd":false},{"ty":"fl","c":{"a":0,"k":[0.105882352941,0.105882352941,0.113725490196,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[190,310]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,-14.958],[-7.517,-4.813],[0,0],[-8.054,0],[0,8.055],[0,0],[0,9.588],[14.958,0]],"o":[[0,9.588],[0,0],[0,8.055],[8.054,0],[0,0],[7.517,-4.813],[0,-14.958],[-14.957,0]],"v":[[-27.083,-19.792],[-14.583,3.034],[-14.583,32.291],[0,46.875],[14.583,32.291],[14.583,3.034],[27.083,-19.792],[0,-46.875]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.105882352941,0.105882352941,0.113725490196,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[187.083,311.875]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"k":[{"s":[0],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[0],"t":900,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":0,"op":900.000036657751,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 7b57ba7e02..0e2559eb1a 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -362,6 +362,26 @@ @string/CustomExpireTimerSelectorView__weeks + + @string/ScreenLockSettingsFragment__immediately + @string/ScreenLockSettingsFragment__after_1_min + @string/ScreenLockSettingsFragment__after_5_min + @string/ScreenLockSettingsFragment__after_30_min + + + + 0 + 60 + 300 + 1800 + + + + @string/CustomExpireTimerSelectorView__minutes + @string/CustomExpireTimerSelectorView__hours + @string/CustomExpireTimerSelectorView__days + + @string/PermissionsSettingsFragment__only_admins @string/PermissionsSettingsFragment__all_members diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cc50759c4..7e6f503a67 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4278,6 +4278,8 @@ Screen lock Lock Signal access with Android screen lock or fingerprint Screen lock inactivity timeout + + To use screen lock set a PIN, pattern, or password on this device. Signal PIN Create a PIN Change your PIN @@ -4298,8 +4300,41 @@ You\'ve made too many attempts. Please try again later. Error connecting to service Backups - Signal is locked - TAP TO UNLOCK + + Unlock Signal + + Use your Android device lock settings to unlock Signal. + + Screen lock is on and Signal is safely secured with your device lock settings. Unlock Signal the same way as you would normally unlock your phone, this could be with Face Unlock, Fingerprint Unlock, a PIN, password or pattern. + + Contact support + + Try again + + + + Off + + Use screen lock + + Your Android device lock settings will be required to unlock Signal when you leave or switch apps. When locked, notification previews will not show message content. + + Start screen lock + + Immediately + + After 1 minute + + After 5 minutes + + After 30 minutes + + Custom time + + Use Signal screen lock + + Turn off Signal screen lock + Unknown Deleted account