Update screen lock.

This commit is contained in:
Michelle Tang
2024-08-08 14:24:08 -04:00
committed by mtang-signal
parent c880db0f4a
commit 3bdbd69a7d
40 changed files with 744 additions and 259 deletions

View File

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

View File

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

View File

@@ -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<String, Int>() {

View File

@@ -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;

View File

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

View File

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

View File

@@ -109,8 +109,13 @@ class LearnMoreTextPreferenceViewHolder(itemView: View) : PreferenceViewHolder<L
class ClickPreferenceViewHolder(itemView: View) : PreferenceViewHolder<ClickPreference>(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 }
}
}
}

View File

@@ -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())
}

View File

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

View File

@@ -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)),

View File

@@ -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()
}
}

View File

@@ -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<String>
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<String> = LocalContext.current.resources.getStringArray(R.array.ScreenLockSettingsFragment__labels).toList()
val values: List<Long> = 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 = {}
)
}
}

View File

@@ -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
)

View File

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

View File

@@ -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<ClickPreference>()
class LongClickPreference(

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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<String> 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, "");

View File

@@ -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")

View File

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

View File

@@ -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) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 B

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<group>
<clip-path
android:pathData="M18,0L54,0A18,18 0,0 1,72 18L72,54A18,18 0,0 1,54 72L18,72A18,18 0,0 1,0 54L0,18A18,18 0,0 1,18 0z"/>
<path
android:pathData="M16,0L56,0A16,16 0,0 1,72 16L72,56A16,16 0,0 1,56 72L16,72A16,16 0,0 1,0 56L0,16A16,16 0,0 1,16 0z"
android:fillColor="#D2D8FE"/>
<group>
<clip-path
android:pathData="M16,0L56,0A16,16 0,0 1,72 16L72,56A16,16 0,0 1,56 72L16,72A16,16 0,0 1,0 56L0,16A16,16 0,0 1,16 0z"/>
<path
android:pathData="M70.973,33.982C71.634,34.534 72,35.357 72,36.217V65.684C72,68.998 69.314,71.684 66,71.684H6C2.686,71.684 0,68.998 0,65.684V37.984C0,37.183 0.317,36.412 0.901,35.863C10.002,27.319 22.811,22 37,22C50.117,22 62.054,26.546 70.973,33.982Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M24,11L48,11A8,8 0,0 1,56 19L56,83A8,8 0,0 1,48 91L24,91A8,8 0,0 1,16 83L16,19A8,8 0,0 1,24 11z"
android:strokeWidth="2.75"
android:fillColor="#E3E8FE"
android:strokeColor="#020BAC"/>
<path
android:pathData="M27.056,33.97C27.056,29.016 31.06,25 36,25C40.94,25 44.944,29.016 44.944,33.97V38.058C45.81,38.535 46.514,39.262 46.963,40.146C47.269,40.749 47.39,41.388 47.446,42.078C47.5,42.741 47.5,43.554 47.5,44.534V50.497C47.5,51.477 47.5,52.29 47.446,52.953C47.39,53.643 47.269,54.282 46.963,54.885C46.49,55.815 45.736,56.571 44.809,57.045C44.208,57.352 43.571,57.473 42.883,57.529C42.222,57.583 41.411,57.583 40.433,57.583H31.567C30.589,57.583 29.778,57.583 29.117,57.529C28.429,57.473 27.792,57.352 27.191,57.045C26.264,56.571 25.51,55.815 25.037,54.885C24.731,54.282 24.61,53.643 24.554,52.953C24.5,52.29 24.5,51.477 24.5,50.497V44.534C24.5,43.554 24.5,42.741 24.554,42.078C24.61,41.388 24.731,40.749 25.037,40.146C25.486,39.262 26.19,38.535 27.056,38.058V33.97ZM42.389,33.97V37.472C41.829,37.447 41.18,37.448 40.433,37.448H31.567C30.82,37.448 30.171,37.447 29.611,37.472V33.97C29.611,30.431 32.472,27.563 36,27.563C39.528,27.563 42.389,30.431 42.389,33.97Z"
android:fillColor="#99A3EC"
android:fillType="evenOdd"/>
<path
android:pathData="M42.389,37.47C42.347,37.469 42.304,37.467 42.261,37.465C41.731,37.446 41.124,37.446 40.433,37.446H31.567C30.876,37.446 30.269,37.446 29.739,37.465C29.696,37.467 29.653,37.469 29.611,37.47V33.969C29.611,30.431 32.472,27.563 36,27.563C39.528,27.563 42.389,30.431 42.389,33.969V37.47ZM26.928,38.13C26.121,38.606 25.464,39.304 25.037,40.145C24.731,40.747 24.61,41.386 24.554,42.076C24.5,42.739 24.5,43.552 24.5,44.532V50.494C24.5,51.475 24.5,52.287 24.554,52.95C24.61,53.64 24.731,54.279 25.037,54.882C25.51,55.812 26.264,56.568 27.191,57.041C27.792,57.348 28.429,57.47 29.117,57.526C29.778,57.58 30.589,57.58 31.566,57.58H40.433C41.411,57.58 42.222,57.58 42.883,57.526C43.571,57.47 44.208,57.348 44.809,57.041C45.736,56.568 46.49,55.812 46.963,54.882C47.269,54.279 47.39,53.64 47.446,52.95C47.5,52.287 47.5,51.475 47.5,50.494V44.532C47.5,43.552 47.5,42.739 47.446,42.076C47.39,41.386 47.269,40.747 46.963,40.145C46.536,39.304 45.879,38.606 45.072,38.13C45.03,38.105 44.987,38.081 44.944,38.057V33.969C44.944,29.015 40.94,25 36,25C31.06,25 27.056,29.015 27.056,33.969V38.057C27.013,38.081 26.97,38.105 26.928,38.13ZM43.24,40.101L42.269,40.059C41.782,40.038 41.199,40.038 40.433,40.038H31.567C30.801,40.038 30.218,40.038 29.731,40.059L28.76,40.101L28.385,40.308C27.996,40.522 27.664,40.858 27.446,41.286C27.352,41.472 27.273,41.743 27.229,42.279C27.185,42.818 27.183,43.515 27.183,44.532V50.494C27.183,51.511 27.185,52.208 27.229,52.747C27.273,53.283 27.352,53.555 27.446,53.74C27.675,54.19 28.031,54.539 28.446,54.75C28.597,54.828 28.831,54.902 29.344,54.944C29.867,54.986 30.548,54.988 31.567,54.988H40.434C41.452,54.988 42.133,54.986 42.656,54.944C43.169,54.902 43.403,54.828 43.554,54.75C43.969,54.539 44.325,54.19 44.554,53.74C44.648,53.555 44.727,53.283 44.771,52.747C44.815,52.208 44.817,51.511 44.817,50.494V44.532C44.817,43.515 44.815,42.818 44.771,42.279C44.727,41.743 44.648,41.472 44.554,41.286C44.336,40.858 44.004,40.522 43.615,40.308L43.24,40.101Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
</group>
</group>
</vector>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
tools:viewBindingIgnore="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/prompt_layout"
android:layout_width="match_parent"
@@ -10,80 +10,40 @@
android:orientation="vertical"
android:fitsSystemWindows="true">
<View android:id="@+id/shim"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_centerVertical="true"
android:visibility="invisible"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/login_top_background"
android:layout_above="@id/shim">
</FrameLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_marginTop="20dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_launcher_foreground"
android:layout_gravity="center"/>
</androidx.appcompat.widget.Toolbar>
app:layout_constraintTop_toTopOf="parent"
android:theme="@style/TextSecure.LightActionBar.DarkText"
android:layout_marginTop="20dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:orientation="vertical"
android:background="@drawable/rounded_rectangle"
android:layout_centerInParent="true"
android:padding="20dp"
android:elevation="10dp">
android:gravity="center">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:text="@string/prompt_passphrase_activity__signal_is_locked"
android:gravity="center_horizontal"
android:textSize="25sp"/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/unlock_view"
android:layout_width="72dp"
android:layout_height="72dp"
app:lottie_rawRes="@raw/lottie_unlock" />
<ImageView android:id="@+id/fingerprint_auth_container"
android:src="@drawable/ic_fingerprint_white_48dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_accent_primary"
android:padding="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="60dp"
tools:visibility="visible"/>
<TextView android:id="@+id/lock_screen_auth_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/rounded_rectangle"
android:backgroundTint="@color/signal_accent_primary"
android:textColor="@color/white"
android:gravity="center_horizontal"
android:elevation="3dp"
android:padding="10dp"
android:text="@string/prompt_passphrase_activity__tap_to_unlock"
tools:visibility="gone"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/prompt_passphrase_activity__unlock_signal"
style="@style/Signal.Text.Headline.Medium" />
<RelativeLayout android:id="@+id/password_auth_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="60dp"
android:layout_marginTop="60dp"
tools:visibility="gone">
<EditText android:id="@+id/passphrase_edit"
@@ -147,4 +107,32 @@
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
android:id="@+id/learn_more_text"
style="@style/Signal.Text.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/lock_screen_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginBottom="32dp"
android:paddingHorizontal="72dp"
android:text="@string/prompt_passphrase_activity__use_your_android_device"
android:textAlignment="center"
android:textColor="@color/signal_colorOnSurfaceVariant"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/lock_screen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Signal.Widget.Button.Medium.Tonal"
android:text="@string/prompt_passphrase_activity__unlock"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="80dp"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -4,7 +4,4 @@
<item android:title="@string/preferences__submit_debug_log"
android:id="@+id/menu_submit_debug_logs"
android:icon="@android:drawable/ic_menu_upload" />
<item android:title="@string/PinRestoreEntryFragment_contact_support"
android:id="@+id/menu_contact_support" />
</menu>

View File

@@ -42,6 +42,13 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_privacySettingsFragment_to_screenLockSettingsFragment"
app:destination="@id/screenLockSettingsFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
@@ -60,6 +67,19 @@
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.pnp.PhoneNumberPrivacySettingsFragment"
android:label="phone_number_privacy_settings_fragment" />
<fragment
android:id="@+id/screenLockSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.screenlock.ScreenLockSettingsFragment"
android:label="screen_lock_settings_fragment">
<action
android:id="@+id/action_screenLockSettingsFragment_to_customScreenLockTimerSelectDialog"
app:destination="@id/customScreenLockTimerSelectDialog" />
</fragment>
<dialog
android:id="@+id/customScreenLockTimerSelectDialog"
android:name="org.thoughtcrime.securesms.components.settings.app.privacy.screenlock.CustomScreenLockTimerSelectDialog" />
<include app:graph="@navigation/app_settings_expire_timer" />
</navigation>

View File

@@ -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":[]}

View File

@@ -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":[]}

View File

@@ -362,6 +362,26 @@
<item>@string/CustomExpireTimerSelectorView__weeks</item>
</string-array>
<string-array name="ScreenLockSettingsFragment__labels">
<item>@string/ScreenLockSettingsFragment__immediately</item>
<item>@string/ScreenLockSettingsFragment__after_1_min</item>
<item>@string/ScreenLockSettingsFragment__after_5_min</item>
<item>@string/ScreenLockSettingsFragment__after_30_min</item>
</string-array>
<integer-array name="ScreenLockSettingsFragment__values">
<item>0</item>
<item>60</item>
<item>300</item>
<item>1800</item>
</integer-array>
<string-array name="CustomScreenLockTimerSelectorView__unit_labels">
<item>@string/CustomExpireTimerSelectorView__minutes</item>
<item>@string/CustomExpireTimerSelectorView__hours</item>
<item>@string/CustomExpireTimerSelectorView__days</item>
</string-array>
<string-array name="PermissionsSettingsFragment__editor_labels">
<item>@string/PermissionsSettingsFragment__only_admins</item>
<item>@string/PermissionsSettingsFragment__all_members</item>

View File

@@ -4278,6 +4278,8 @@
<string name="preferences_app_protection__screen_lock">Screen lock</string>
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Lock Signal access with Android screen lock or fingerprint</string>
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Screen lock inactivity timeout</string>
<!-- Snackbar text shown when trying to enable screen lock but the user does not have a password or biometrics enabled -->
<string name="preferences_app_protection__to_use_screen_lock">To use screen lock set a PIN, pattern, or password on this device.</string>
<string name="preferences_app_protection__signal_pin">Signal PIN</string>
<string name="preferences_app_protection__create_a_pin">Create a PIN</string>
<string name="preferences_app_protection__change_your_pin">Change your PIN</string>
@@ -4298,8 +4300,41 @@
<string name="RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later">You\'ve made too many attempts. Please try again later.</string>
<string name="RegistrationActivity_error_connecting_to_service">Error connecting to service</string>
<string name="preferences_chats__backups">Backups</string>
<string name="prompt_passphrase_activity__signal_is_locked">Signal is locked</string>
<string name="prompt_passphrase_activity__tap_to_unlock">TAP TO UNLOCK</string>
<!-- Title text shown when Signal is locked and needs to be unlocked -->
<string name="prompt_passphrase_activity__unlock_signal">Unlock Signal</string>
<!-- Description text explaining how to unlock Signal -->
<string name="prompt_passphrase_activity__use_your_android_device">Use your Android device lock settings to unlock Signal.</string>
<!-- Text shown in a dialog that further explains how to unlock Signal by using the same unlocking methods that are used to unlock their own device -->
<string name="prompt_passphrase_activity__screen_lock_is_on">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.</string>
<!-- Button in a dialog that will contact Signal support if pressed -->
<string name="prompt_passphrase_activity__contact_support">Contact support</string>
<!-- Button text to try again after unlocking has previously failed -->
<string name="prompt_passphrase_activity__try_again">Try again</string>
<!-- ScreenLockSettingsFragment -->
<!-- Text shown when screen lock is not enabled -->
<string name="ScreenLockSettingsFragment__off">Off</string>
<!-- Text shown with toggle that when switched on will enable screen lock -->
<string name="ScreenLockSettingsFragment__use_screen_lock">Use screen lock</string>
<!-- Description of what screen locking will do and how notification content will not be shown when screen is locked -->
<string name="ScreenLockSettingsFragment__your_android_device">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.</string>
<!-- Title text explaining when users should start the screen lock -->
<string name="ScreenLockSettingsFragment__start_screen_lock">Start screen lock</string>
<!-- Option text explaining that screen lock should start immediately -->
<string name="ScreenLockSettingsFragment__immediately">Immediately</string>
<!-- Option text explaining that screen lock should start after 1 minute of inactivity -->
<string name="ScreenLockSettingsFragment__after_1_min">After 1 minute</string>
<!-- Option text explaining that screen lock should start after 5 minutes of inactivity -->
<string name="ScreenLockSettingsFragment__after_5_min">After 5 minutes</string>
<!-- Option text explaining that screen lock should start after 30 minutes of inactivity -->
<string name="ScreenLockSettingsFragment__after_30_min">After 30 minutes</string>
<!-- Option text explaining that screen lock should start at a custom time set by the user -->
<string name="ScreenLockSettingsFragment__custom_time">Custom time</string>
<!-- Title on biometrics prompt explaining that biometrics are needed to turn on screen lock -->
<string name="ScreenLockSettingsFragment__use_signal_screen_lock">Use Signal screen lock</string>
<!-- Title on biometrics prompt explaining that biometrics are needed to turn off screen lock -->
<string name="ScreenLockSettingsFragment__turn_off_signal_lock">Turn off Signal screen lock</string>
<string name="Recipient_unknown">Unknown</string>
<!-- Name to use for a user across the UI when they are unregistered and have no other name available -->
<string name="Recipient_deleted_account">Deleted account</string>