Improve auto lock and make it more customizable

This patch makes the auto lock option more customizable. Users can now choose a
combination of the following: Locking Aegis when
- The back button is pressed
- The app is minimized
- The device is locked

<img src="https://alexbakker.me/u/rlj4y2u8pk.png" width="300">
This commit is contained in:
Alexander Bakker 2020-08-12 21:33:25 +02:00
parent 7d38bc9b71
commit d875cb6baa
28 changed files with 146 additions and 88 deletions

View file

@ -12,7 +12,12 @@ import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.beemdevelopment.aegis.services.NotificationService;
import com.beemdevelopment.aegis.ui.MainActivity;
@ -53,6 +58,9 @@ public class AegisApplication extends Application {
intentFilter.addAction(CODE_LOCK_VAULT_ACTION);
registerReceiver(receiver, intentFilter);
// lock the app if the user moves the application to the background
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
@ -111,8 +119,8 @@ public class AegisApplication extends Application {
return _prefs;
}
public boolean isAutoLockEnabled() {
return _prefs.isAutoLockEnabled() && !isVaultLocked() && _manager.isEncryptionEnabled() ;
public boolean isAutoLockEnabled(int autoLockType) {
return _prefs.isAutoLockTypeEnabled(autoLockType) && !isVaultLocked() && _manager.isEncryptionEnabled();
}
public void registerLockListener(LockListener listener) {
@ -123,10 +131,14 @@ public class AegisApplication extends Application {
_lockListeners.remove(listener);
}
public void lock() {
/**
* Locks the vault and the app.
* @param userInitiated whether or not the user initiated the lock in MainActivity.
*/
public void lock(boolean userInitiated) {
_manager = null;
for (LockListener listener : _lockListeners) {
listener.onLocked();
listener.onLocked(userInitiated);
}
stopService(new Intent(AegisApplication.this, NotificationService.class));
@ -168,16 +180,29 @@ public class AegisApplication extends Application {
}
}
public class ScreenOffReceiver extends BroadcastReceiver {
private class AppLifecycleObserver implements LifecycleEventObserver {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP && isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)) {
lock(false);
}
}
}
private class ScreenOffReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (isAutoLockEnabled()) {
lock();
if (isAutoLockEnabled(Preferences.AUTO_LOCK_ON_DEVICE_LOCK)) {
lock(false);
}
}
}
public interface LockListener {
void onLocked();
/**
* When called, the app/vault has been locked and the listener should perform its cleanup operations.
* @param userInitiated whether or not the user initiated the lock in MainActivity.
*/
void onLocked(boolean userInitiated);
}
}

View file

@ -1,7 +0,0 @@
package com.beemdevelopment.aegis;
public enum CancelAction {
KILL,
CLOSE
}

View file

@ -12,6 +12,17 @@ import java.util.Locale;
import java.util.concurrent.TimeUnit;
public class Preferences {
public static final int AUTO_LOCK_OFF = 1 << 0;
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
public static final int[] AUTO_LOCK_SETTINGS = {
AUTO_LOCK_ON_BACK_BUTTON,
AUTO_LOCK_ON_MINIMIZE,
AUTO_LOCK_ON_DEVICE_LOCK
};
private SharedPreferences _prefs;
public Preferences(Context context) {
@ -73,8 +84,25 @@ public class Preferences {
return _prefs.getBoolean("pref_intro", false);
}
private int getAutoLockMask() {
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
if (!_prefs.contains("pref_auto_lock_mask")) {
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
}
return _prefs.getInt("pref_auto_lock_mask", def);
}
public boolean isAutoLockEnabled() {
return _prefs.getBoolean("pref_auto_lock", true);
return getAutoLockMask() != AUTO_LOCK_OFF;
}
public boolean isAutoLockTypeEnabled(int autoLockType) {
return (getAutoLockMask() & autoLockType) == autoLockType;
}
public void setAutoLockMask(int autoLock) {
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
}
public void setIntroDone(boolean done) {

View file

@ -6,7 +6,6 @@ import android.os.Bundle;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.CallSuper;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.AegisApplication;
@ -20,9 +19,7 @@ import java.util.Locale;
import java.util.Map;
public abstract class AegisActivity extends AppCompatActivity implements AegisApplication.LockListener {
private boolean _resumed;
private AegisApplication _app;
private Theme _configuredTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -39,6 +36,7 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
return;
}
@ -58,24 +56,9 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
@Override
protected void onResume() {
super.onResume();
_resumed = true;
}
@Override
protected void onPause() {
super.onPause();
_resumed = false;
}
@CallSuper
@Override
public void onLocked() {
if (isOrphan()) {
setResult(RESULT_CANCELED, null);
finish();
}
public void onLocked(boolean userInitiated) {
setResult(RESULT_CANCELED, null);
finishAndRemoveTask();
}
protected AegisApplication getApp() {
@ -139,13 +122,6 @@ public abstract class AegisActivity extends AppCompatActivity implements AegisAp
}
}
/**
* Reports whether this Activity has been resumed. (i.e. onResume was called)
*/
protected boolean isOpen() {
return _resumed;
}
/**
* Reports whether this Activity instance has become an orphan. This can happen if
* the vault was locked by an external trigger while the Activity was still open.

View file

@ -23,7 +23,6 @@ import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ThemeMap;
@ -52,9 +51,7 @@ import javax.crypto.SecretKey;
public class AuthActivity extends AegisActivity {
private EditText _textPassword;
private CancelAction _cancelAction;
private SlotList _slots;
private SecretKey _bioKey;
private BiometricSlot _bioSlot;
private BiometricPrompt _bioPrompt;
@ -62,7 +59,7 @@ public class AuthActivity extends AegisActivity {
private int _failedUnlockAttempts;
// the first time this activity is resumed after creation, it's possible to inhibit showing the
// biometric prompt by setting 'inhibitBioPrompt' to false through the intent
// biometric prompt by setting 'inhibitBioPrompt' to true through the intent
private boolean _inhibitBioPrompt;
private Preferences _prefs;
@ -95,7 +92,6 @@ public class AuthActivity extends AegisActivity {
} else {
_inhibitBioPrompt = savedInstanceState.getBoolean("inhibitBioPrompt", false);
}
_cancelAction = (CancelAction) intent.getSerializableExtra("cancelAction");
_slots = (SlotList) intent.getSerializableExtra("slots");
_stateless = _slots != null;
if (!_stateless) {
@ -182,11 +178,10 @@ public class AuthActivity extends AegisActivity {
@Override
public void onBackPressed() {
switch (_cancelAction) {
case KILL:
finishAffinity();
case CLOSE:
finish();
if (_stateless) {
super.onBackPressed();
} else {
finishAffinity();
}
}

View file

@ -25,7 +25,7 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.SortCategory;
import com.beemdevelopment.aegis.ViewMode;
@ -523,8 +523,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return;
}
if (_app.isAutoLockEnabled()) {
_app.lock();
if (_app.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_BACK_BUTTON)) {
_app.lock(false);
return;
}
@ -596,7 +596,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
return true;
}
case R.id.action_lock:
_app.lock();
_app.lock(true);
return true;
default:
if (item.getGroupId() == R.id.action_filter_group) {
@ -655,7 +655,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private void startAuthActivity(boolean inhibitBioPrompt) {
if (!_isAuthenticating) {
Intent intent = new Intent(this, AuthActivity.class);
intent.putExtra("cancelAction", CancelAction.KILL);
intent.putExtra("inhibitBioPrompt", inhibitBioPrompt);
startActivityForResult(intent, CODE_DECRYPT);
_isAuthenticating = true;
@ -747,7 +746,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
public void onListChange() { _fabScrollHelper.setVisible(true); }
@Override
public void onLocked() {
public void onLocked(boolean userInitiated) {
if (_actionMode != null) {
_actionMode.finish();
}
@ -755,11 +754,11 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_entryListView.clearEntries();
_loaded = false;
if (isOpen()) {
if (userInitiated) {
startAuthActivity(true);
} else {
super.onLocked(userInitiated);
}
super.onLocked();
}
private void copyEntryCode(VaultEntry entry) {

View file

@ -20,7 +20,6 @@ import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.AegisApplication;
import com.beemdevelopment.aegis.BuildConfig;
import com.beemdevelopment.aegis.CancelAction;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
@ -235,7 +234,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_result.putExtra("needsRefresh", true);
return true;
});
Preference entryHighlightPreference = findPreference("pref_highlight_entry");
entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_result.putExtra("needsRefresh", true);
@ -254,7 +253,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
public boolean onPreferenceChange(Preference preference, Object newValue) {
_result.putExtra("needsRecreate", true);
Window window = getActivity().getWindow();
if ((boolean)newValue) {
if ((boolean) newValue) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
@ -458,6 +457,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
});
_autoLockPreference = findPreference("pref_auto_lock");
_autoLockPreference.setSummary(getAutoLockSummary());
_autoLockPreference.setOnPreferenceClickListener((preference) -> {
final int[] items = Preferences.AUTO_LOCK_SETTINGS;
final String[] textItems = getResources().getStringArray(R.array.pref_auto_lock_types);
final boolean[] checkedItems = new boolean[items.length];
for (int i = 0; i < items.length; i++) {
checkedItems[i] = _prefs.isAutoLockTypeEnabled(items[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
.setTitle(R.string.pref_auto_lock_prompt)
.setMultiChoiceItems(textItems, checkedItems, (dialog, index, isChecked) -> checkedItems[index] = isChecked)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int autoLock = Preferences.AUTO_LOCK_OFF;
for (int i = 0; i < checkedItems.length; i++) {
if (checkedItems[i]) {
autoLock |= items[i];
}
}
_prefs.setAutoLockMask(autoLock);
_autoLockPreference.setSummary(getAutoLockSummary());
})
.setNegativeButton(android.R.string.cancel, null);
Dialogs.showSecureDialog(builder.create());
return false;
});
_passwordReminderPreference = findPreference("pref_password_reminder");
}
@ -575,7 +603,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
Intent intent = new Intent(getActivity(), AuthActivity.class);
intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots());
intent.putExtra("cancelAction", CancelAction.CLOSE);
startActivityForResult(intent, CODE_IMPORT_DECRYPT);
} else {
state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() {
@ -842,6 +869,28 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
_pinKeyboardPreference.setChecked(enable);
}
private String getAutoLockSummary() {
final int[] settings = Preferences.AUTO_LOCK_SETTINGS;
final String[] descriptions = getResources().getStringArray(R.array.pref_auto_lock_types);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < settings.length; i++) {
if (_prefs.isAutoLockTypeEnabled(settings[i])) {
if (builder.length() != 0) {
builder.append(", ");
}
builder.append(descriptions[i].toLowerCase());
}
}
if (builder.length() == 0) {
return getString(R.string.pref_auto_lock_summary_disabled);
}
return getString(R.string.pref_auto_lock_summary, builder.toString());
}
private class SetPasswordListener implements Dialogs.SlotListener {
@Override
public void onSlotResult(Slot slot, Cipher cipher) {