diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f0e7bfa30c..75e147c44b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -19,6 +19,7 @@ + @@ -262,10 +263,8 @@ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_rectangle_white.xml b/res/drawable/rounded_rectangle_white.xml new file mode 100644 index 0000000000..337bd7d8dc --- /dev/null +++ b/res/drawable/rounded_rectangle_white.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/prompt_passphrase_activity.xml b/res/layout/prompt_passphrase_activity.xml index 68de241ad3..fe49d9b01e 100644 --- a/res/layout/prompt_passphrase_activity.xml +++ b/res/layout/prompt_passphrase_activity.xml @@ -1,28 +1,76 @@ - + + + + + android:background="?login_top_background" + android:layout_above="@id/shim"> - + - + - + + + + + + android:layout_marginLeft="30dp" + android:layout_marginRight="30dp" + android:orientation="vertical" + android:background="?login_floating_background" + android:layout_centerInParent="true" + android:padding="20dp" + android:elevation="10dp"> + + + + + + - - - \ No newline at end of file + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index e55e02204b..cf044a6035 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -134,6 +134,9 @@ + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index fe226aac13..15bbcb2210 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -39,7 +39,7 @@ off Off SMS %1$s, MMS %2$s - Passphrase %1$s, Screen security %2$s + Screen lock %1$s, Screen security %2$s Theme %1$s, Language %2$s @@ -1360,6 +1360,11 @@ Wrong number? Never Unknown + Unlock Signal + Screen lock + Lock Signal access with Android screen lock or fingerprint + Screen lock inactivity timeout + None diff --git a/res/values/themes.xml b/res/values/themes.xml index b1cbdfe2e7..f174636de2 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -58,23 +58,28 @@ + diff --git a/res/xml/preferences_app_protection.xml b/res/xml/preferences_app_protection.xml index 771c365753..36f541de89 100644 --- a/res/xml/preferences_app_protection.xml +++ b/res/xml/preferences_app_protection.xml @@ -2,6 +2,17 @@ + + + + + = Build.VERSION_CODES.JELLY_BEAN && !keyguardManager.isKeyguardSecure()) { + Log.w(TAG ,"Keyguard not secure..."); + handleAuthenticated(); + return; + } + + if (Build.VERSION.SDK_INT >= 16 && fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints()) { + Log.w(TAG, "Listening for fingerprints..."); + fingerprintCancellationSignal = new CancellationSignal(); + fingerprintManager.authenticate(null, 0, fingerprintCancellationSignal, fingerprintListener, null); + } else if (Build.VERSION.SDK_INT >= 21){ + Log.w(TAG, "firing intent..."); + Intent intent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock Signal", ""); + startActivityForResult(intent, 1); + } else { + Log.w(TAG, "Not compatible..."); + handleAuthenticated(); + } + } + + private void pauseScreenLock() { + if (Build.VERSION.SDK_INT >= 16 && fingerprintCancellationSignal != null) { + fingerprintCancellationSignal.cancel(); + } } private class PassphraseActionListener implements TextView.OnEditorActionListener { @@ -202,4 +310,57 @@ public class PassphrasePromptActivity extends PassphraseActivity { this.passphraseText.setText(""); System.gc(); } + + private class FingerprintListener extends FingerprintManagerCompat.AuthenticationCallback { + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + Log.w(TAG, "Authentication error: " + errMsgId + " " + errString); + onAuthenticationFailed(); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) { + Log.w(TAG, "onAuthenticationSucceeded"); + fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp); + 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() { + @Override + public void onAnimationEnd(Animator animation) { + handleAuthenticated(); + + fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp); + fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN); + } + }).start(); + } + + @Override + public void onAuthenticationFailed() { + Log.w(TAG, "onAuthenticatoinFailed()"); + FingerprintManagerCompat.AuthenticationCallback callback = this; + + fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp); + 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_primary), PorterDuff.Mode.SRC_IN); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + + fingerprintPrompt.startAnimation(shake); + } + + } } diff --git a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 358ca0cc96..92c321d4f3 100644 --- a/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/src/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.preferences; +import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AlertDialog; @@ -18,6 +20,7 @@ import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.BlockedContactsActivity; import org.thoughtcrime.securesms.PassphraseChangeActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.jobs.MultiDeviceReadReceiptUpdateJob; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -37,16 +40,16 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment disablePassphrase = (CheckBoxPreference) this.findPreference("pref_enable_passphrase_temporary"); - this.findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF) - .setOnPreferenceClickListener(new ChangePassphraseClickListener()); - this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF) - .setOnPreferenceClickListener(new PassphraseIntervalClickListener()); - this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF) - .setOnPreferenceChangeListener(new ReadReceiptToggleListener()); - this.findPreference(PREFERENCE_CATEGORY_BLOCKED) - .setOnPreferenceClickListener(new BlockedContactsClickListener()); - disablePassphrase - .setOnPreferenceChangeListener(new DisablePassphraseClickListener()); + this.findPreference(TextSecurePreferences.SCREEN_LOCK).setOnPreferenceChangeListener(new ScreenLockListener()); + this.findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setOnPreferenceClickListener(new ScreenLockTimeoutListener()); + + this.findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF).setOnPreferenceClickListener(new ChangePassphraseClickListener()); + this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF).setOnPreferenceClickListener(new PassphraseIntervalClickListener()); + this.findPreference(TextSecurePreferences.READ_RECEIPTS_PREF).setOnPreferenceChangeListener(new ReadReceiptToggleListener()); + this.findPreference(PREFERENCE_CATEGORY_BLOCKED).setOnPreferenceClickListener(new BlockedContactsClickListener()); + disablePassphrase.setOnPreferenceChangeListener(new DisablePassphraseClickListener()); + + initializeVisibility(); } @Override @@ -59,17 +62,88 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment super.onResume(); ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__privacy); - initializeTimeoutSummary(); + if (!TextSecurePreferences.isPasswordDisabled(getContext())) initializePassphraseTimeoutSummary(); + else initializeScreenLockTimeoutSummary(); disablePassphrase.setChecked(!TextSecurePreferences.isPasswordDisabled(getActivity())); } - private void initializeTimeoutSummary() { + private void initializePassphraseTimeoutSummary() { int timeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(getActivity()); this.findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF) .setSummary(getResources().getQuantityString(R.plurals.AppProtectionPreferenceFragment_minutes, timeoutMinutes, timeoutMinutes)); } + private void initializeScreenLockTimeoutSummary() { + long timeoutSeconds = TextSecurePreferences.getScreenLockTimeout(getContext()); + long hours = TimeUnit.SECONDS.toHours(timeoutSeconds); + long minutes = TimeUnit.SECONDS.toMinutes(timeoutSeconds) - (TimeUnit.SECONDS.toHours(timeoutSeconds) * 60 ); + long seconds = TimeUnit.SECONDS.toSeconds(timeoutSeconds) - (TimeUnit.SECONDS.toMinutes(timeoutSeconds) * 60); + + findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT) + .setSummary(timeoutSeconds <= 0 ? getString(R.string.AppProtectionPreferenceFragment_none) : + String.format("%02d:%02d:%02d", hours, minutes, seconds)); + } + + private void initializeVisibility() { + if (TextSecurePreferences.isPasswordDisabled(getContext())) { + findPreference("pref_enable_passphrase_temporary").setVisible(false); + findPreference(TextSecurePreferences.CHANGE_PASSPHRASE_PREF).setVisible(false); + findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_INTERVAL_PREF).setVisible(false); + findPreference(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF).setVisible(false); + + KeyguardManager keyguardManager = (KeyguardManager)getContext().getSystemService(Context.KEYGUARD_SERVICE); + if (Build.VERSION.SDK_INT < 16 || !keyguardManager.isKeyguardSecure()) { + ((SwitchPreferenceCompat)findPreference(TextSecurePreferences.SCREEN_LOCK)).setChecked(false); + findPreference(TextSecurePreferences.SCREEN_LOCK).setEnabled(false); + } + } else { + findPreference(TextSecurePreferences.SCREEN_LOCK).setVisible(false); + findPreference(TextSecurePreferences.SCREEN_LOCK_TIMEOUT).setVisible(false); + } + } + + private class ScreenLockListener implements Preference.OnPreferenceChangeListener { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + TextSecurePreferences.setScreenLockEnabled(getContext(), (Boolean)newValue); + + Intent intent = new Intent(getContext(), KeyCachingService.class); + intent.setAction(KeyCachingService.CLEAR_KEY_EVENT); + getContext().startService(intent); + + return true; + } + } + + private class ScreenLockTimeoutListener implements Preference.OnPreferenceClickListener, HmsPickerDialogFragment.HmsPickerDialogHandler { + + @Override + public boolean onPreferenceClick(Preference preference) { + int[] attributes = {R.attr.app_protect_timeout_picker_color}; + TypedArray hmsStyle = getActivity().obtainStyledAttributes(attributes); + + new HmsPickerBuilder().setFragmentManager(getFragmentManager()) + .setStyleResId(hmsStyle.getResourceId(0, R.style.BetterPickersDialogFragment_Light)) + .addHmsPickerDialogHandler(this) + .show(); + + hmsStyle.recycle(); + + return true; + } + + @Override + public void onDialogHmsSet(int reference, int hours, int minutes, int seconds) { + long timeoutSeconds = Math.max(TimeUnit.HOURS.toSeconds(hours) + + TimeUnit.MINUTES.toSeconds(minutes) + + TimeUnit.SECONDS.toSeconds(seconds), 60); + + TextSecurePreferences.setScreenLockTimeout(getContext(), timeoutSeconds); + initializeScreenLockTimeoutSummary(); + } + } + private class BlockedContactsClickListener implements Preference.OnPreferenceClickListener { @Override public boolean onPreferenceClick(Preference preference) { @@ -118,7 +192,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment (int)TimeUnit.SECONDS.toMinutes(seconds), 1); TextSecurePreferences.setPassphraseTimeoutInterval(getActivity(), timeoutMinutes); - initializeTimeoutSummary(); + initializePassphraseTimeoutSummary(); } } @@ -142,6 +216,8 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment Intent intent = new Intent(getActivity(), KeyCachingService.class); intent.setAction(KeyCachingService.DISABLE_ACTION); getActivity().startService(intent); + + initializeVisibility(); }); builder.setNegativeButton(android.R.string.cancel, null); builder.show(); @@ -171,7 +247,7 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment final String onRes = context.getString(R.string.ApplicationPreferencesActivity_on); final String offRes = context.getString(R.string.ApplicationPreferencesActivity_off); - if (TextSecurePreferences.isPasswordDisabled(context)) { + if (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context)) { if (TextSecurePreferences.isScreenSecurityEnabled(context)) { return context.getString(privacySummaryResId, offRes, onRes); } else { diff --git a/src/org/thoughtcrime/securesms/service/KeyCachingService.java b/src/org/thoughtcrime/securesms/service/KeyCachingService.java index 6861f2b8a5..fd78b45cd5 100644 --- a/src/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/src/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -83,7 +83,7 @@ public class KeyCachingService extends Service { } public static synchronized @Nullable MasterSecret getMasterSecret(Context context) { - if (masterSecret == null && TextSecurePreferences.isPasswordDisabled(context)) { + if (masterSecret == null && (TextSecurePreferences.isPasswordDisabled(context) && !TextSecurePreferences.isScreenLockEnabled(context))) { try { MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(context, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); Intent intent = new Intent(context, KeyCachingService.class); @@ -146,7 +146,7 @@ public class KeyCachingService extends Service { this.pending = PendingIntent.getService(this, 0, new Intent(PASSPHRASE_EXPIRED_EVENT, null, this, KeyCachingService.class), 0); - if (TextSecurePreferences.isPasswordDisabled(this)) { + if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) { try { MasterSecret masterSecret = MasterSecretUtil.getMasterSecret(this, MasterSecretUtil.UNENCRYPTED_PASSPHRASE); setMasterSecret(masterSecret); @@ -210,8 +210,11 @@ public class KeyCachingService extends Service { } private void handleDisableService() { - if (TextSecurePreferences.isPasswordDisabled(this)) + if (TextSecurePreferences.isPasswordDisabled(this) && + !TextSecurePreferences.isScreenLockEnabled(this)) + { stopForeground(true); + } } private void handleLocaleChanged() { @@ -221,10 +224,19 @@ public class KeyCachingService extends Service { private void startTimeoutIfAppropriate() { boolean timeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(this); + long screenTimeout = TextSecurePreferences.getScreenLockTimeout(this); - if ((activitiesRunning == 0) && (KeyCachingService.masterSecret != null) && timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(this)) { - long timeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(this); - long timeoutMillis = TimeUnit.MINUTES.toMillis(timeoutMinutes); + if ((activitiesRunning == 0) && (KeyCachingService.masterSecret != null) && + (timeoutEnabled && !TextSecurePreferences.isPasswordDisabled(this)) || + (screenTimeout >= 60 && TextSecurePreferences.isScreenLockEnabled(this))) + { + long passphraseTimeoutMinutes = TextSecurePreferences.getPassphraseTimeoutInterval(this); + long screenLockTimeoutSeconds = TextSecurePreferences.getScreenLockTimeout(this); + + long timeoutMillis; + + if (!TextSecurePreferences.isPasswordDisabled(this)) timeoutMillis = TimeUnit.MINUTES.toMillis(passphraseTimeoutMinutes); + else timeoutMillis = TimeUnit.SECONDS.toMillis(screenLockTimeoutSeconds); Log.w("KeyCachingService", "Starting timeout: " + timeoutMillis); @@ -280,7 +292,7 @@ public class KeyCachingService extends Service { } private void foregroundService() { - if (TextSecurePreferences.isPasswordDisabled(this)) { + if (TextSecurePreferences.isPasswordDisabled(this) && !TextSecurePreferences.isScreenLockEnabled(this)) { stopForeground(true); return; } diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index e98669d0d1..21c06c6846 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -62,7 +62,7 @@ public class TextSecurePreferences { public static final String ALL_MMS_PREF = "pref_all_mms"; public static final String ALL_SMS_PREF = "pref_all_sms"; public static final String PASSPHRASE_TIMEOUT_INTERVAL_PREF = "pref_timeout_interval"; - private static final String PASSPHRASE_TIMEOUT_PREF = "pref_timeout_passphrase"; + public static final String PASSPHRASE_TIMEOUT_PREF = "pref_timeout_passphrase"; public static final String SCREEN_SECURITY_PREF = "pref_screen_security"; private static final String ENTER_SENDS_PREF = "pref_enter_sends"; private static final String ENTER_PRESENT_PREF = "pref_enter_key"; @@ -142,6 +142,25 @@ public class TextSecurePreferences { private static final String BACKUP_TIME = "pref_backup_next_time"; public static final String BACKUP_NOW = "pref_backup_create"; + public static final String SCREEN_LOCK = "pref_android_screen_lock"; + public static final String SCREEN_LOCK_TIMEOUT = "pref_android_screen_lock_timeout"; + + public static boolean isScreenLockEnabled(@NonNull Context context) { + return getBooleanPreference(context, SCREEN_LOCK, false); + } + + public static void setScreenLockEnabled(@NonNull Context context, boolean value) { + setBooleanPreference(context, SCREEN_LOCK, value); + } + + public static long getScreenLockTimeout(@NonNull Context context) { + return getLongPreference(context, SCREEN_LOCK_TIMEOUT, 0); + } + + public static void setScreenLockTimeout(@NonNull Context context, long value) { + setLongPreference(context, SCREEN_LOCK_TIMEOUT, value); + } + public static void setBackupPassphrase(@NonNull Context context, @Nullable String passphrase) { setStringPreference(context, BACKUP_PASSPHRASE, passphrase); }