From ef8c116bdc9cabc5486a1257724fe76d22a180a2 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Wed, 13 Jan 2021 21:03:14 +0100 Subject: [PATCH] Split preferences into separate fragments --- app/proguard-rules.pro | 1 + .../beemdevelopment/aegis/OverallTest.java | 1 + .../aegis/ui/MainActivity.java | 13 +- .../aegis/ui/PreferencesActivity.java | 82 +- .../aegis/ui/PreferencesFragment.java | 1166 ----------------- .../AppearancePreferencesFragment.java | 132 ++ .../fragments/BackupsPreferencesFragment.java | 127 ++ .../BehaviorPreferencesFragment.java | 26 + .../ui/fragments/MainPreferencesFragment.java | 13 + .../ui/fragments/PreferencesFragment.java | 89 ++ .../SecurityPreferencesFragment.java | 410 ++++++ .../fragments/ToolsPreferencesFragment.java | 485 +++++++ app/src/main/res/anim/slide_in_left.xml | 25 + app/src/main/res/anim/slide_in_right.xml | 25 + app/src/main/res/anim/slide_out_left.xml | 25 + app/src/main/res/anim/slide_out_right.xml | 25 + .../main/res/drawable/ic_brush_black_24dp.xml | 8 + .../ic_cloud_upload_outline_black_24dp.xml | 8 + .../main/res/drawable/ic_gesture_tap_24dp.xml | 8 + .../main/res/drawable/ic_tools_black_24dp.xml | 8 + app/src/main/res/values-ar-rSA/strings.xml | 1 - app/src/main/res/values-cs-rCZ/strings.xml | 1 - app/src/main/res/values-de-rDE/strings.xml | 1 - app/src/main/res/values-el-rGR/strings.xml | 1 - app/src/main/res/values-es-rES/strings.xml | 1 - app/src/main/res/values-eu-rES/strings.xml | 1 - app/src/main/res/values-fa-rIR/strings.xml | 1 - app/src/main/res/values-fi-rFI/strings.xml | 1 - app/src/main/res/values-fr-rFR/strings.xml | 1 - app/src/main/res/values-hi-rIN/strings.xml | 1 - app/src/main/res/values-hu-rHU/strings.xml | 1 - app/src/main/res/values-in-rID/strings.xml | 1 - app/src/main/res/values-it-rIT/strings.xml | 1 - app/src/main/res/values-ja-rJP/strings.xml | 1 - app/src/main/res/values-kn-rIN/strings.xml | 1 - app/src/main/res/values-nl-rNL/strings.xml | 1 - app/src/main/res/values-pl-rPL/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt-rPT/strings.xml | 1 - app/src/main/res/values-ro-rRO/strings.xml | 1 - app/src/main/res/values-ru-rRU/strings.xml | 1 - app/src/main/res/values-sk-rSK/strings.xml | 1 - app/src/main/res/values-tr-rTR/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/styles.xml | 13 +- app/src/main/res/xml/preferences.xml | 210 +-- .../main/res/xml/preferences_appearance.xml | 45 + app/src/main/res/xml/preferences_backups.xml | 25 + app/src/main/res/xml/preferences_behavior.xml | 23 + app/src/main/res/xml/preferences_security.xml | 72 + app/src/main/res/xml/preferences_tools.xml | 20 + 53 files changed, 1706 insertions(+), 1406 deletions(-) delete mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/AppearancePreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BehaviorPreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/MainPreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/PreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java create mode 100644 app/src/main/java/com/beemdevelopment/aegis/ui/fragments/ToolsPreferencesFragment.java create mode 100644 app/src/main/res/anim/slide_in_left.xml create mode 100644 app/src/main/res/anim/slide_in_right.xml create mode 100644 app/src/main/res/anim/slide_out_left.xml create mode 100644 app/src/main/res/anim/slide_out_right.xml create mode 100644 app/src/main/res/drawable/ic_brush_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_cloud_upload_outline_black_24dp.xml create mode 100644 app/src/main/res/drawable/ic_gesture_tap_24dp.xml create mode 100644 app/src/main/res/drawable/ic_tools_black_24dp.xml create mode 100644 app/src/main/res/xml/preferences_appearance.xml create mode 100644 app/src/main/res/xml/preferences_backups.xml create mode 100644 app/src/main/res/xml/preferences_behavior.xml create mode 100644 app/src/main/res/xml/preferences_security.xml create mode 100644 app/src/main/res/xml/preferences_tools.xml diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e84a179b..163e5b45 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,6 +18,7 @@ -keepclasseswithmembers public class androidx.recyclerview.widget.RecyclerView { *; } +-keep class com.beemdevelopment.aegis.ui.fragments.* -keep class com.beemdevelopment.aegis.importers.** { *; } -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index a95e939a..32067aaf 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -127,6 +127,7 @@ public class OverallTest extends AegisTest { openContextualActionModeOverflowMenu(); onView(withText(R.string.action_settings)).perform(click()); + onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_security_group_title)), click())); onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(R.string.pref_encryption_title)), click())); onView(withId(android.R.id.button1)).perform(click()); diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 214afa89..54ab4dd2 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -32,6 +32,8 @@ import com.beemdevelopment.aegis.helpers.FabScrollHelper; import com.beemdevelopment.aegis.helpers.PermissionHelper; import com.beemdevelopment.aegis.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.GoogleAuthInfoException; +import com.beemdevelopment.aegis.ui.fragments.BackupsPreferencesFragment; +import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment; import com.beemdevelopment.aegis.ui.views.EntryListView; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultFile; @@ -141,7 +143,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene _btnBackupError = findViewById(R.id.btn_backup_error); _btnBackupError.setOnClickListener(view -> { - startPreferencesActivity("pref_backups"); + startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups"); }); _fabScrollHelper = new FabScrollHelper(fab); @@ -402,8 +404,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene startActivityForResult(chooserIntent, CODE_SCAN_IMAGE); } - private void startPreferencesActivity(String preference) { + private void startPreferencesActivity() { + startPreferencesActivity(null, null); + } + + private void startPreferencesActivity(Class fragmentType, String preference) { Intent intent = new Intent(this, PreferencesActivity.class); + intent.putExtra("fragment", fragmentType); intent.putExtra("pref", preference); startActivityForResult(intent, CODE_PREFERENCES); } @@ -577,7 +584,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: { - startPreferencesActivity(null); + startPreferencesActivity(); return true; } case R.id.action_about: { diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesActivity.java index e97d1663..a843aa7e 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesActivity.java @@ -1,10 +1,18 @@ package com.beemdevelopment.aegis.ui; -import android.content.Intent; import android.os.Bundle; import android.view.MenuItem; -public class PreferencesActivity extends AegisActivity { +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; + +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.ui.fragments.MainPreferencesFragment; +import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment; + +public class PreferencesActivity extends AegisActivity implements + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { private PreferencesFragment _fragment; @Override @@ -17,28 +25,31 @@ public class PreferencesActivity extends AegisActivity { } if (savedInstanceState == null) { - _fragment = new PreferencesFragment(); + _fragment = new MainPreferencesFragment(); _fragment.setArguments(getIntent().getExtras()); - getSupportFragmentManager().beginTransaction().replace(android.R.id.content, _fragment).commit(); + + getSupportFragmentManager().beginTransaction() + .replace(android.R.id.content, _fragment) + .commit(); + + PreferencesFragment requestedFragment = getRequestedFragment(); + if (requestedFragment != null) { + _fragment = requestedFragment; + showFragment(_fragment); + } } else { _fragment = (PreferencesFragment) getSupportFragmentManager().findFragmentById(android.R.id.content); } } @Override - public void onResume() { - super.onResume(); - - Intent intent = getIntent(); - String preference = intent.getStringExtra("pref"); - if (preference != null) { - _fragment.scrollToPreference(preference); - intent.removeExtra("pref"); - } + public void onBackPressed() { + super.onBackPressed(); + setTitle(R.string.action_settings); } @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // pass permission request results to the fragment _fragment.onRequestPermissionsResult(requestCode, permissions, grantResults); } @@ -62,14 +73,45 @@ public class PreferencesActivity extends AegisActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - break; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + } else { + return super.onOptionsItemSelected(item); } return true; } + + @Override + public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) { + _fragment = (PreferencesFragment) getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment()); + _fragment.setArguments(pref.getExtras()); + _fragment.setTargetFragment(caller, 0); + showFragment(_fragment); + + setTitle(pref.getTitle()); + return true; + } + + private void showFragment(PreferencesFragment fragment) { + getSupportFragmentManager().beginTransaction() + .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right) + .replace(android.R.id.content, fragment) + .addToBackStack(null) + .commit(); + } + + @SuppressWarnings("unchecked") + private PreferencesFragment getRequestedFragment() { + Class fragmentType = (Class) getIntent().getSerializableExtra("fragment"); + if (fragmentType == null) { + return null; + } + + try { + return fragmentType.newInstance(); + } catch (IllegalAccessException | InstantiationException e) { + throw new RuntimeException(e); + } + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java deleted file mode 100644 index 9366e257..00000000 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/PreferencesFragment.java +++ /dev/null @@ -1,1166 +0,0 @@ -package com.beemdevelopment.aegis.ui; - -import android.app.Activity; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.biometric.BiometricPrompt; -import androidx.core.content.FileProvider; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreferenceCompat; - -import com.beemdevelopment.aegis.AegisApplication; -import com.beemdevelopment.aegis.BuildConfig; -import com.beemdevelopment.aegis.Preferences; -import com.beemdevelopment.aegis.R; -import com.beemdevelopment.aegis.Theme; -import com.beemdevelopment.aegis.ViewMode; -import com.beemdevelopment.aegis.crypto.KeyStoreHandle; -import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; -import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer; -import com.beemdevelopment.aegis.helpers.BiometricsHelper; -import com.beemdevelopment.aegis.helpers.SpinnerHelper; -import com.beemdevelopment.aegis.importers.AegisImporter; -import com.beemdevelopment.aegis.importers.DatabaseImporter; -import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException; -import com.beemdevelopment.aegis.importers.DatabaseImporterException; -import com.beemdevelopment.aegis.services.NotificationService; -import com.beemdevelopment.aegis.ui.models.ImportEntry; -import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; -import com.beemdevelopment.aegis.ui.tasks.ExportTask; -import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask; -import com.beemdevelopment.aegis.util.UUIDMap; -import com.beemdevelopment.aegis.vault.VaultBackupManager; -import com.beemdevelopment.aegis.vault.VaultEntry; -import com.beemdevelopment.aegis.vault.VaultFileCredentials; -import com.beemdevelopment.aegis.vault.VaultFileException; -import com.beemdevelopment.aegis.vault.VaultManager; -import com.beemdevelopment.aegis.vault.VaultManagerException; -import com.beemdevelopment.aegis.vault.slots.BiometricSlot; -import com.beemdevelopment.aegis.vault.slots.PasswordSlot; -import com.beemdevelopment.aegis.vault.slots.Slot; -import com.beemdevelopment.aegis.vault.slots.SlotException; -import com.beemdevelopment.aegis.vault.slots.SlotList; -import com.topjohnwu.superuser.Shell; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -import javax.crypto.Cipher; - -import static android.text.TextUtils.isDigitsOnly; - -public class PreferencesFragment extends PreferenceFragmentCompat { - // activity request codes - private static final int CODE_IMPORT = 0; - private static final int CODE_IMPORT_DECRYPT = 1; - private static final int CODE_SLOTS = 2; - private static final int CODE_GROUPS = 3; - private static final int CODE_SELECT_ENTRIES = 4; - private static final int CODE_EXPORT = 5; - private static final int CODE_EXPORT_PLAIN = 6; - private static final int CODE_EXPORT_GOOGLE_URI = 7; - private static final int CODE_BACKUPS = 8; - - private Intent _result; - private AegisApplication _app; - private Preferences _prefs; - private VaultManager _vault; - - // keep a reference to the type of database converter the user selected - private Class _importerType; - private AegisImporter.State _importerState; - private UUIDMap _importerEntries; - - private SwitchPreference _encryptionPreference; - private SwitchPreference _biometricsPreference; - private Preference _autoLockPreference; - private Preference _setPasswordPreference; - private Preference _slotsPreference; - private Preference _groupsPreference; - private Preference _passwordReminderPreference; - private SwitchPreferenceCompat _pinKeyboardPreference; - private SwitchPreferenceCompat _backupsPreference; - private Preference _backupsLocationPreference; - private Preference _backupsTriggerPreference; - private Preference _backupsVersionsPreference; - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.preferences); - - _app = (AegisApplication) getActivity().getApplication(); - _prefs = _app.getPreferences(); - _vault = _app.getVaultManager(); - - // set the result intent in advance - setResult(new Intent()); - - int currentTheme = _prefs.getCurrentTheme().ordinal(); - Preference darkModePreference = findPreference("pref_dark_mode"); - darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme])); - darkModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - int currentTheme = _prefs.getCurrentTheme().ordinal(); - - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.choose_theme) - .setSingleChoiceItems(R.array.theme_titles, currentTheme, (dialog, which) -> { - int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - _prefs.setCurrentTheme(Theme.fromInteger(i)); - - dialog.dismiss(); - - _result.putExtra("needsRecreate", true); - getActivity().recreate(); - }) - .setNegativeButton(android.R.string.cancel, null) - .create()); - - return true; - } - }); - - Preference langPreference = findPreference("pref_lang"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - langPreference.setOnPreferenceChangeListener((preference, newValue) -> { - _result.putExtra("needsRecreate", true); - getActivity().recreate(); - return true; - }); - } else { - // Setting locale doesn't work on Marshmallow or below - langPreference.setVisible(false); - } - - int currentViewMode = _prefs.getCurrentViewMode().ordinal(); - Preference viewModePreference = findPreference("pref_view_mode"); - viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode])); - viewModePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - int currentViewMode = _prefs.getCurrentViewMode().ordinal(); - - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.choose_view_mode) - .setSingleChoiceItems(R.array.view_mode_titles, currentViewMode, (dialog, which) -> { - int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); - _prefs.setCurrentViewMode(ViewMode.fromInteger(i)); - viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i])); - _result.putExtra("needsRefresh", true); - dialog.dismiss(); - }) - .setNegativeButton(android.R.string.cancel, null) - .create()); - - return true; - } - }); - - Preference importPreference = findPreference("pref_import"); - importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Dialogs.showImportersDialog(getContext(), false, definition -> { - _importerType = definition.getType(); - - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - startActivityForResult(intent, CODE_IMPORT); - }); - return true; - } - }); - - Preference importAppPreference = findPreference("pref_import_app"); - importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Dialogs.showImportersDialog(getContext(), true, definition -> { - DatabaseImporter importer = DatabaseImporter.create(getContext(), definition.getType()); - importApp(importer); - }); - return true; - } - }); - - Preference exportPreference = findPreference("pref_export"); - exportPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - startExport(); - return true; - } - }); - - /*EditTextPreference timeoutPreference = (EditTextPreference) findPreference("pref_timeout"); - timeoutPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - preference.setSummary(String.format(getString(R.string.pref_timeout_summary), (String) newValue)); - return true; - } - }); - timeoutPreference.getOnPreferenceChangeListener().onPreferenceChange(timeoutPreference, timeoutPreference.getText());*/ - - Preference codeDigitGroupingPreference = findPreference("pref_code_group_size"); - codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> { - _result.putExtra("needsRefresh", true); - return true; - }); - - Preference issuerPreference = findPreference("pref_account_name"); - issuerPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - _result.putExtra("needsRefresh", true); - return true; - } - }); - - Preference copyOnTapPreference = findPreference("pref_copy_on_tap"); - copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> { - _result.putExtra("needsRefresh", true); - return true; - }); - - Preference entryHighlightPreference = findPreference("pref_highlight_entry"); - entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> { - _result.putExtra("needsRefresh", true); - return true; - }); - - Preference tapToRevealPreference = findPreference("pref_tap_to_reveal"); - tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> { - _result.putExtra("needsRefresh", true); - return true; - }); - - Preference screenPreference = findPreference("pref_secure_screen"); - screenPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - _result.putExtra("needsRecreate", true); - Window window = getActivity().getWindow(); - if ((boolean) newValue) { - window.addFlags(WindowManager.LayoutParams.FLAG_SECURE); - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE); - } - return true; - } - }); - - Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time"); - tapToRevealTimePreference.setSummary(_app.getPreferences().getTapToRevealTime() + " seconds"); - tapToRevealTimePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Dialogs.showNumberPickerDialog(getActivity(), new Dialogs.NumberInputListener() { - @Override - public void onNumberInputResult(int number) { - _app.getPreferences().setTapToRevealTime(number); - tapToRevealTimePreference.setSummary(number + " seconds"); - _result.putExtra("needsRefresh", true); - } - }); - return false; - } - }); - - _encryptionPreference = (SwitchPreference) findPreference("pref_encryption"); - _encryptionPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (!_vault.isEncryptionEnabled()) { - Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener()); - } else { - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.disable_encryption) - .setMessage(getString(R.string.disable_encryption_description)) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - try { - _vault.disableEncryption(); - } catch (VaultManagerException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e); - return; - } - - // clear the KeyStore - try { - KeyStoreHandle handle = new KeyStoreHandle(); - handle.clear(); - } catch (KeyStoreHandleException e) { - e.printStackTrace(); - } - - getActivity().stopService(new Intent(getActivity(), NotificationService.class)); - updateEncryptionPreferences(); - } - }) - .setNegativeButton(android.R.string.no, null) - .create()); - } - return false; - } - }); - - _biometricsPreference = (SwitchPreference) findPreference("pref_biometrics"); - _biometricsPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - VaultFileCredentials creds = _vault.getCredentials(); - SlotList slots = creds.getSlots(); - - if (!slots.has(BiometricSlot.class)) { - if (BiometricsHelper.isAvailable(getContext())) { - BiometricSlotInitializer initializer = new BiometricSlotInitializer(PreferencesFragment.this, new RegisterBiometricsListener()); - BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() - .setTitle(getString(R.string.set_up_biometric)) - .setNegativeButtonText(getString(android.R.string.cancel)) - .build(); - initializer.authenticate(info); - } - } else { - // remove the biometric slot - BiometricSlot slot = slots.find(BiometricSlot.class); - slots.remove(slot); - _vault.setCredentials(creds); - - // remove the KeyStore key - try { - KeyStoreHandle handle = new KeyStoreHandle(); - handle.deleteKey(slot.getUUID().toString()); - } catch (KeyStoreHandleException e) { - e.printStackTrace(); - } - - saveVault(); - updateEncryptionPreferences(); - } - - return false; - } - }); - - _setPasswordPreference = findPreference("pref_password"); - _setPasswordPreference.setOnPreferenceClickListener(preference -> { - Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener()); - return false; - }); - - _slotsPreference = findPreference("pref_slots"); - _slotsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(getActivity(), SlotManagerActivity.class); - intent.putExtra("creds", _vault.getCredentials()); - startActivityForResult(intent, CODE_SLOTS); - return true; - } - }); - - _pinKeyboardPreference = findPreference("pref_pin_keyboard"); - _pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> { - if (!(boolean) newValue) { - return true; - } - - Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> { - if (isDigitsOnly(new String(password))) { - List slots = _vault.getCredentials().getSlots().findAll(PasswordSlot.class); - PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password); - PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener()); - task.execute(getLifecycle(), params); - } else { - setPinKeyboardPreference(false); - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.pin_keyboard_error) - .setMessage(R.string.pin_keyboard_error_description) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, null) - .create()); - } - }, dialog -> { - setPinKeyboardPreference(false); - }); - return false; - }); - - _groupsPreference = findPreference("pref_groups"); - _groupsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(getActivity(), GroupManagerActivity.class); - intent.putExtra("groups", new ArrayList<>(_vault.getGroups())); - startActivityForResult(intent, CODE_GROUPS); - return true; - } - }); - - _backupsPreference = findPreference("pref_backups"); - _backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { - if ((boolean) newValue) { - selectBackupsLocation(); - } else { - _prefs.setIsBackupsEnabled(false); - updateBackupPreference(); - } - - return false; - }); - - Uri backupLocation = _prefs.getBackupsLocation(); - _backupsLocationPreference = findPreference("pref_backups_location"); - if (backupLocation != null) { - _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString()))); - } - _backupsLocationPreference.setOnPreferenceClickListener(preference -> { - selectBackupsLocation(); - return false; - }); - - _backupsTriggerPreference = findPreference("pref_backups_trigger"); - _backupsTriggerPreference.setOnPreferenceClickListener(preference -> { - if (_prefs.isBackupsEnabled()) { - try { - _vault.backup(); - Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show(); - } catch (VaultManagerException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.backup_error, e); - } - } - return true; - }); - - _backupsVersionsPreference = findPreference("pref_backups_versions"); - _backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount())); - _backupsVersionsPreference.setOnPreferenceClickListener(preference -> { - Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> { - number = number * 5 + 5; - _prefs.setBackupsVersionCount(number); - _backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, _prefs.getBackupsVersionCount(), _prefs.getBackupsVersionCount())); - }); - return false; - }); - - _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"); - } - - @Override - public void onResume() { - super.onResume(); - updateEncryptionPreferences(); - updateBackupPreference(); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (data == null) { - return; - } - - switch (requestCode) { - case CODE_IMPORT: - onImportResult(resultCode, data); - break; - case CODE_IMPORT_DECRYPT: - onImportDecryptResult(resultCode, data); - break; - case CODE_SLOTS: - onSlotManagerResult(resultCode, data); - break; - case CODE_GROUPS: - onGroupManagerResult(resultCode, data); - break; - case CODE_SELECT_ENTRIES: - onSelectEntriesResult(resultCode, data); - break; - case CODE_EXPORT: - // intentional fallthrough - case CODE_EXPORT_PLAIN: - // intentional fallthrough - case CODE_EXPORT_GOOGLE_URI: - onExportResult(requestCode, resultCode, data); - break; - case CODE_BACKUPS: - onSelectBackupsLocationResult(resultCode, data); - break; - } - } - - public Intent getResult() { - return _result; - } - - public void setResult(Intent result) { - _result = result; - getActivity().setResult(Activity.RESULT_OK, _result); - } - - private void importApp(DatabaseImporter importer) { - // obtain the global root shell and close it immediately after we're done - // TODO: find a way to use SuFileInputStream with Shell.newInstance() - try (Shell shell = Shell.getShell()) { - if (!shell.isRoot()) { - Toast.makeText(getActivity(), R.string.root_error, Toast.LENGTH_SHORT).show(); - return; - } - - DatabaseImporter.State state = importer.readFromApp(); - processImporterState(state); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - Toast.makeText(getActivity(), R.string.app_lookup_error, Toast.LENGTH_SHORT).show(); - } catch (IOException | DatabaseImporterException e) { - e.printStackTrace(); - Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show(); - } - } - - private void processImporterState(DatabaseImporter.State state) { - try { - if (state.isEncrypted()) { - // temporary special case for encrypted Aegis vaults - if (state instanceof AegisImporter.EncryptedState) { - _importerState = state; - - Intent intent = new Intent(getActivity(), AuthActivity.class); - intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots()); - startActivityForResult(intent, CODE_IMPORT_DECRYPT); - } else { - state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() { - @Override - public void onStateDecrypted(DatabaseImporter.State state) { - importDatabase(state); - } - - @Override - public void onError(Exception e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e); - } - }); - } - } else { - importDatabase(state); - } - } catch (DatabaseImporterException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e); - } - } - - private void onImportDecryptResult(int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - _importerState = null; - return; - } - - VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds"); - DatabaseImporter.State state; - try { - state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds); - } catch (DatabaseImporterException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e); - return; - } - - importDatabase(state); - _importerState = null; - } - - private void onImportResult(int resultCode, Intent data) { - Uri uri = data.getData(); - if (resultCode != Activity.RESULT_OK || uri == null) { - return; - } - - try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) { - DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType); - DatabaseImporter.State state = importer.read(stream); - processImporterState(state); - } catch (FileNotFoundException e) { - Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show(); - } catch (DatabaseImporterException | IOException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e); - } - } - - private void importDatabase(DatabaseImporter.State state) { - DatabaseImporter.Result result; - try { - result = state.convert(); - } catch (DatabaseImporterException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e); - return; - } - - _importerEntries = result.getEntries(); - List entries = new ArrayList<>(); - for (VaultEntry entry : _importerEntries) { - entries.add(new ImportEntry(entry)); - } - - Intent intent = new Intent(getActivity(), SelectEntriesActivity.class); - intent.putExtra("entries", (ArrayList) entries); - intent.putExtra("errors", (ArrayList) result.getErrors()); - intent.putExtra("vaultContainsEntries", _vault.getEntries().size() > 0); - startActivityForResult(intent, CODE_SELECT_ENTRIES); - } - - private void startExport() { - View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null); - TextView warningText = view.findViewById(R.id.text_export_warning); - CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt); - CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept); - Spinner spinner = view.findViewById(R.id.spinner_export_format); - SpinnerHelper.fillSpinner(getContext(), spinner, R.array.export_formats); - spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - checkBoxEncrypt.setChecked(position == 0); - checkBoxEncrypt.setEnabled(position == 0); - warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE); - } - - @Override - public void onNothingSelected(AdapterView parent) { - - } - }); - - AlertDialog dialog = new AlertDialog.Builder(getContext()) - .setTitle(R.string.pref_export_summary) - .setView(view) - .setNeutralButton(R.string.share, null) - .setPositiveButton(android.R.string.ok, null) - .setNegativeButton(android.R.string.cancel, null) - .create(); - - dialog.setOnShowListener(d -> { - Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); - - checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> { - warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE); - checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE); - checkBoxAccept.setChecked(false); - btnPos.setEnabled(isChecked); - btnNeutral.setEnabled(isChecked); - }); - - checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> { - btnPos.setEnabled(isChecked); - btnNeutral.setEnabled(isChecked); - }); - - btnPos.setOnClickListener(v -> { - dialog.dismiss(); - - if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) { - return; - } - - int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); - VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); - Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType(getExportMimeType(requestCode)) - .putExtra(Intent.EXTRA_TITLE, fileInfo.toString()); - startActivityForResult(intent, requestCode); - }); - - btnNeutral.setOnClickListener(v -> { - dialog.dismiss(); - - if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) { - return; - } - - File file; - try { - VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); - file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir()); - } catch (IOException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); - return; - } - - int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); - startExportVault(requestCode, cb -> { - try (OutputStream stream = new FileOutputStream(file)) { - cb.exportVault(stream); - } catch (IOException | VaultManagerException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); - return; - } - - Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file); - Intent intent = new Intent(Intent.ACTION_SEND) - .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .setType(getExportMimeType(requestCode)) - .putExtra(Intent.EXTRA_STREAM, uri); - Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary)); - startActivity(chooser); - }); - }); - }); - - Dialogs.showSecureDialog(dialog); - } - - private void onSlotManagerResult(int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; - } - - VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds"); - _vault.setCredentials(creds); - saveVault(); - updateEncryptionPreferences(); - } - - private void onGroupManagerResult(int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; - } - - HashSet groups = new HashSet<>(data.getStringArrayListExtra("groups")); - - for (VaultEntry entry : _vault.getEntries()) { - if (!groups.contains(entry.getGroup())) { - entry.setGroup(null); - } - } - - saveVault(); - } - - private void onSelectEntriesResult(int resultCode, Intent data) { - if (resultCode != Activity.RESULT_OK) { - return; - } - - boolean wipeEntries = data.getBooleanExtra("wipeEntries", false); - if (wipeEntries) { - _vault.wipeEntries(); - } - - List selectedEntries = (ArrayList) data.getSerializableExtra("entries"); - for (ImportEntry selectedEntry : selectedEntries) { - VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID()); - - // temporary: randomize the UUID of duplicate entries and add them anyway - if (_vault.isEntryDuplicate(savedEntry)) { - savedEntry.resetUUID(); - } - - _vault.addEntry(savedEntry); - } - - _importerEntries = null; - if (!saveVault()) { - return; - } - - String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size()); - Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show(); - - _result.putExtra("needsRecreate", true); - } - - private static int getExportRequestCode(int spinnerPos, boolean encrypt) { - if (spinnerPos == 0) { - return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN; - } - - return CODE_EXPORT_GOOGLE_URI; - } - - private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) { - if (spinnerPos == 0) { - String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN; - return new VaultBackupManager.FileInfo(filename); - } - - return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt"); - } - - private static String getExportMimeType(int requestCode) { - return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json"; - } - - private File getExportCacheDir() throws IOException { - File dir = new File(getContext().getCacheDir(), "export"); - if (!dir.exists() && !dir.mkdir()) { - throw new IOException(String.format("Unable to create directory %s", dir)); - } - - return dir; - } - - private void startExportVault(int requestCode, StartExportCallback cb) { - switch (requestCode) { - case CODE_EXPORT: - if (_vault.isEncryptionEnabled()) { - cb.exportVault(stream -> _vault.export(stream)); - } else { - Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() { - @Override - public void onSlotResult(Slot slot, Cipher cipher) { - VaultFileCredentials creds = new VaultFileCredentials(); - - try { - slot.setKey(creds.getKey(), cipher); - creds.getSlots().add(slot); - } catch (SlotException e) { - onException(e); - return; - } - - cb.exportVault(stream -> _vault.export(stream, creds)); - } - - @Override - public void onException(Exception e) { - - } - }); - } - break; - case CODE_EXPORT_PLAIN: - cb.exportVault((stream) -> _vault.export(stream, null)); - break; - case CODE_EXPORT_GOOGLE_URI: - cb.exportVault((stream) -> _vault.exportGoogleUris(stream)); - break; - } - } - - private void onExportResult(int requestCode, int resultCode, Intent data) { - Uri uri = data.getData(); - if (resultCode != Activity.RESULT_OK || uri == null) { - return; - } - - - startExportVault(requestCode, cb -> { - File file; - OutputStream outStream = null; - try { - file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir()); - outStream = new FileOutputStream(file); - cb.exportVault(outStream); - - new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri)); - } catch (VaultManagerException | IOException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); - } finally { - try { - if (outStream != null) { - outStream.close(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - }); - } - - private void onSelectBackupsLocationResult(int resultCode, Intent data) { - Uri uri = data.getData(); - if (resultCode != Activity.RESULT_OK || uri == null) { - return; - } - - int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags); - - _prefs.setBackupsLocation(uri); - _prefs.setIsBackupsEnabled(true); - _prefs.setBackupsError(null); - _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString()))); - updateBackupPreference(); - } - - private boolean saveVault() { - try { - _vault.save(true); - } catch (VaultManagerException e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.saving_error, e); - return false; - } - - return true; - } - - private void updateEncryptionPreferences() { - boolean encrypted = _vault.isEncryptionEnabled(); - _encryptionPreference.setChecked(encrypted, true); - _setPasswordPreference.setVisible(encrypted); - _biometricsPreference.setVisible(encrypted); - _slotsPreference.setEnabled(encrypted); - _autoLockPreference.setVisible(encrypted); - _pinKeyboardPreference.setVisible(encrypted); - - if (encrypted) { - SlotList slots = _vault.getCredentials().getSlots(); - boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1; - boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1; - boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio; - boolean canUseBio = BiometricsHelper.isAvailable(getContext()); - _setPasswordPreference.setEnabled(!multiPassword); - _biometricsPreference.setEnabled(canUseBio && !multiBio); - _biometricsPreference.setChecked(slots.has(BiometricSlot.class), true); - _slotsPreference.setVisible(showSlots); - _passwordReminderPreference.setVisible(slots.has(BiometricSlot.class)); - } else { - _setPasswordPreference.setEnabled(false); - _biometricsPreference.setEnabled(false); - _biometricsPreference.setChecked(false, true); - _slotsPreference.setVisible(false); - _passwordReminderPreference.setVisible(false); - } - - updateBackupPreference(); - } - - private void updateBackupPreference() { - boolean encrypted = _vault.isEncryptionEnabled(); - boolean backupEnabled = _prefs.isBackupsEnabled() && encrypted; - _backupsPreference.setChecked(backupEnabled); - _backupsPreference.setEnabled(encrypted); - _backupsLocationPreference.setVisible(backupEnabled); - _backupsTriggerPreference.setVisible(backupEnabled); - _backupsVersionsPreference.setVisible(backupEnabled); - } - - private void selectBackupsLocation() { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); - startActivityForResult(intent, CODE_BACKUPS); - } - - private void setPinKeyboardPreference(boolean enable) { - _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) { - VaultFileCredentials creds = _vault.getCredentials(); - SlotList slots = creds.getSlots(); - - try { - // encrypt the master key for this slot - slot.setKey(creds.getKey(), cipher); - - // remove the old master password slot - PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class); - if (oldSlot != null) { - slots.remove(oldSlot); - } - - // add the new master password slot - slots.add(slot); - } catch (SlotException e) { - onException(e); - return; - } - - _vault.setCredentials(creds); - saveVault(); - - if (_prefs.isPinKeyboardEnabled()) { - setPinKeyboardPreference(false); - Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onException(Exception e) { - e.printStackTrace(); - updateEncryptionPreferences(); - Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e); - } - } - - private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener { - @Override - public void onInitializeSlot(BiometricSlot slot, Cipher cipher) { - VaultFileCredentials creds = _vault.getCredentials(); - try { - slot.setKey(creds.getKey(), cipher); - } catch (SlotException e) { - e.printStackTrace(); - onSlotInitializationFailed(0, e.toString()); - return; - } - creds.getSlots().add(slot); - _vault.setCredentials(creds); - - saveVault(); - updateEncryptionPreferences(); - } - - @Override - public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) { - if (!BiometricsHelper.isCanceled(errorCode)) { - Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString); - } - } - } - - private class EnableEncryptionListener implements Dialogs.SlotListener { - @Override - public void onSlotResult(Slot slot, Cipher cipher) { - VaultFileCredentials creds = new VaultFileCredentials(); - - try { - slot.setKey(creds.getKey(), cipher); - creds.getSlots().add(slot); - _vault.enableEncryption(creds); - } catch (VaultManagerException | SlotException e) { - onException(e); - return; - } - - getActivity().startService(new Intent(getActivity(), NotificationService.class)); - updateEncryptionPreferences(); - } - - @Override - public void onException(Exception e) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e); - } - } - - private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback { - @Override - public void onTaskFinished(PasswordSlotDecryptTask.Result result) { - if (result != null) { - setPinKeyboardPreference(true); - } else { - Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) - .setTitle(R.string.pin_keyboard_error) - .setMessage(R.string.invalid_password) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, null) - .create()); - setPinKeyboardPreference(false); - } - } - } - - private class ExportResultListener implements ExportTask.Callback { - @Override - public void onTaskFinished(Exception e) { - if (e != null) { - e.printStackTrace(); - Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); - } else { - Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show(); - } - } - } - - private interface FinishExportCallback { - void exportVault(OutputStream stream) throws IOException, VaultManagerException; - } - - private interface StartExportCallback { - void exportVault(FinishExportCallback exportCb); - } -} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/AppearancePreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/AppearancePreferencesFragment.java new file mode 100644 index 00000000..cc6b5270 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/AppearancePreferencesFragment.java @@ -0,0 +1,132 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import com.beemdevelopment.aegis.Preferences; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.Theme; +import com.beemdevelopment.aegis.ViewMode; +import com.beemdevelopment.aegis.ui.Dialogs; +import com.beemdevelopment.aegis.ui.GroupManagerActivity; +import com.beemdevelopment.aegis.vault.VaultEntry; + +import java.util.ArrayList; +import java.util.HashSet; + +public class AppearancePreferencesFragment extends PreferencesFragment { + private Preference _groupsPreference; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.preferences_appearance); + Preferences prefs = getPreferences(); + + _groupsPreference = findPreference("pref_groups"); + _groupsPreference.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(getActivity(), GroupManagerActivity.class); + intent.putExtra("groups", new ArrayList<>(getVault().getGroups())); + startActivityForResult(intent, CODE_GROUPS); + return true; + }); + + int currentTheme = prefs.getCurrentTheme().ordinal(); + Preference darkModePreference = findPreference("pref_dark_mode"); + darkModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.theme_titles)[currentTheme])); + darkModePreference.setOnPreferenceClickListener(preference -> { + int currentTheme1 = prefs.getCurrentTheme().ordinal(); + + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.choose_theme) + .setSingleChoiceItems(R.array.theme_titles, currentTheme1, (dialog, which) -> { + int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + prefs.setCurrentTheme(Theme.fromInteger(i)); + + dialog.dismiss(); + + getResult().putExtra("needsRecreate", true); + getActivity().recreate(); + }) + .setNegativeButton(android.R.string.cancel, null) + .create()); + + return true; + }); + + Preference langPreference = findPreference("pref_lang"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + langPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRecreate", true); + getActivity().recreate(); + return true; + }); + } else { + // Setting locale doesn't work on Marshmallow or below + langPreference.setVisible(false); + } + + int currentViewMode = prefs.getCurrentViewMode().ordinal(); + Preference viewModePreference = findPreference("pref_view_mode"); + viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[currentViewMode])); + viewModePreference.setOnPreferenceClickListener(preference -> { + int currentViewMode1 = prefs.getCurrentViewMode().ordinal(); + + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.choose_view_mode) + .setSingleChoiceItems(R.array.view_mode_titles, currentViewMode1, (dialog, which) -> { + int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition(); + prefs.setCurrentViewMode(ViewMode.fromInteger(i)); + viewModePreference.setSummary(String.format("%s: %s", getString(R.string.selected), getResources().getStringArray(R.array.view_mode_titles)[i])); + getResult().putExtra("needsRefresh", true); + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, null) + .create()); + + return true; + }); + + Preference codeDigitGroupingPreference = findPreference("pref_code_group_size"); + codeDigitGroupingPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRefresh", true); + return true; + }); + + Preference issuerPreference = findPreference("pref_account_name"); + issuerPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRefresh", true); + return true; + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (data != null && requestCode == CODE_GROUPS) { + onGroupManagerResult(resultCode, data); + } + } + + private void onGroupManagerResult(int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + return; + } + + HashSet groups = new HashSet<>(data.getStringArrayListExtra("groups")); + + for (VaultEntry entry : getVault().getEntries()) { + if (!groups.contains(entry.getGroup())) { + entry.setGroup(null); + } + } + + saveVault(); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java new file mode 100644 index 00000000..e99aa502 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BackupsPreferencesFragment.java @@ -0,0 +1,127 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.preference.Preference; +import androidx.preference.SwitchPreferenceCompat; + +import com.beemdevelopment.aegis.Preferences; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.ui.Dialogs; +import com.beemdevelopment.aegis.vault.VaultManagerException; + +public class BackupsPreferencesFragment extends PreferencesFragment { + private SwitchPreferenceCompat _backupsPreference; + private Preference _backupsLocationPreference; + private Preference _backupsTriggerPreference; + private Preference _backupsVersionsPreference; + + @Override + public void onResume() { + super.onResume(); + updateBackupPreference(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.preferences_backups); + Preferences prefs = getPreferences(); + + _backupsPreference = findPreference("pref_backups"); + _backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if ((boolean) newValue) { + selectBackupsLocation(); + } else { + prefs.setIsBackupsEnabled(false); + updateBackupPreference(); + } + + return false; + }); + + Uri backupLocation = prefs.getBackupsLocation(); + _backupsLocationPreference = findPreference("pref_backups_location"); + if (backupLocation != null) { + _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString()))); + } + _backupsLocationPreference.setOnPreferenceClickListener(preference -> { + selectBackupsLocation(); + return false; + }); + + _backupsTriggerPreference = findPreference("pref_backups_trigger"); + _backupsTriggerPreference.setOnPreferenceClickListener(preference -> { + if (prefs.isBackupsEnabled()) { + try { + getVault().backup(); + Toast.makeText(getActivity(), R.string.backup_successful, Toast.LENGTH_LONG).show(); + } catch (VaultManagerException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.backup_error, e); + } + } + return true; + }); + + _backupsVersionsPreference = findPreference("pref_backups_versions"); + _backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount())); + _backupsVersionsPreference.setOnPreferenceClickListener(preference -> { + Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> { + number = number * 5 + 5; + prefs.setBackupsVersionCount(number); + _backupsVersionsPreference.setSummary(getResources().getQuantityString(R.plurals.pref_backups_versions_summary, prefs.getBackupsVersionCount(), prefs.getBackupsVersionCount())); + }); + return false; + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null && requestCode == CODE_BACKUPS) { + onSelectBackupsLocationResult(resultCode, data); + } + } + + private void onSelectBackupsLocationResult(int resultCode, Intent data) { + Uri uri = data.getData(); + if (resultCode != Activity.RESULT_OK || uri == null) { + return; + } + + int flags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + getContext().getContentResolver().takePersistableUriPermission(data.getData(), flags); + + Preferences prefs = getPreferences(); + prefs.setBackupsLocation(uri); + prefs.setIsBackupsEnabled(true); + prefs.setBackupsError(null); + _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString()))); + updateBackupPreference(); + } + + private void updateBackupPreference() { + boolean encrypted = getVault().isEncryptionEnabled(); + boolean backupEnabled = getPreferences().isBackupsEnabled() && encrypted; + _backupsPreference.setChecked(backupEnabled); + _backupsPreference.setEnabled(encrypted); + _backupsLocationPreference.setVisible(backupEnabled); + _backupsTriggerPreference.setVisible(backupEnabled); + _backupsVersionsPreference.setVisible(backupEnabled); + } + + private void selectBackupsLocation() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + + getApp().setBlockAutoLock(true); + startActivityForResult(intent, CODE_BACKUPS); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BehaviorPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BehaviorPreferencesFragment.java new file mode 100644 index 00000000..733c9c46 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/BehaviorPreferencesFragment.java @@ -0,0 +1,26 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.os.Bundle; + +import androidx.preference.Preference; +import com.beemdevelopment.aegis.R; + +public class BehaviorPreferencesFragment extends PreferencesFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.preferences_behavior); + + Preference copyOnTapPreference = findPreference("pref_copy_on_tap"); + copyOnTapPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRefresh", true); + return true; + }); + + Preference entryHighlightPreference = findPreference("pref_highlight_entry"); + entryHighlightPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRefresh", true); + return true; + }); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/MainPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/MainPreferencesFragment.java new file mode 100644 index 00000000..be183f3b --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/MainPreferencesFragment.java @@ -0,0 +1,13 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.os.Bundle; + +import com.beemdevelopment.aegis.R; + +public class MainPreferencesFragment extends PreferencesFragment { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/PreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/PreferencesFragment.java new file mode 100644 index 00000000..c79e4aab --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/PreferencesFragment.java @@ -0,0 +1,89 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.CallSuper; +import androidx.preference.PreferenceFragmentCompat; + +import com.beemdevelopment.aegis.AegisApplication; +import com.beemdevelopment.aegis.Preferences; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.ui.Dialogs; +import com.beemdevelopment.aegis.vault.VaultManager; +import com.beemdevelopment.aegis.vault.VaultManagerException; + +public abstract class PreferencesFragment extends PreferenceFragmentCompat { + // activity request codes + public static final int CODE_IMPORT = 0; + public static final int CODE_IMPORT_DECRYPT = 1; + public static final int CODE_SLOTS = 2; + public static final int CODE_GROUPS = 3; + public static final int CODE_SELECT_ENTRIES = 4; + public static final int CODE_EXPORT = 5; + public static final int CODE_EXPORT_PLAIN = 6; + public static final int CODE_EXPORT_GOOGLE_URI = 7; + public static final int CODE_BACKUPS = 8; + + private AegisApplication _app; + private Intent _result; + private Preferences _prefs; + private VaultManager _vault; + + @Override + @CallSuper + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + _app = (AegisApplication) getActivity().getApplication(); + _prefs = _app.getPreferences(); + _vault = _app.getVaultManager(); + + setResult(new Intent()); + } + + @Override + @CallSuper + public void onResume() { + super.onResume(); + + Intent intent = getActivity().getIntent(); + String preference = intent.getStringExtra("pref"); + if (preference != null) { + scrollToPreference(preference); + intent.removeExtra("pref"); + } + } + + public Intent getResult() { + return _result; + } + + public void setResult(Intent result) { + _result = result; + getActivity().setResult(Activity.RESULT_OK, _result); + } + + protected AegisApplication getApp() { + return _app; + } + + protected Preferences getPreferences() { + return _prefs; + } + + protected VaultManager getVault() { + return _vault; + } + + protected boolean saveVault() { + try { + _vault.save(true); + } catch (VaultManagerException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.saving_error, e); + return false; + } + + return true; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java new file mode 100644 index 00000000..9764645d --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/SecurityPreferencesFragment.java @@ -0,0 +1,410 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.biometric.BiometricPrompt; +import androidx.preference.Preference; +import androidx.preference.SwitchPreferenceCompat; + +import com.beemdevelopment.aegis.BuildConfig; +import com.beemdevelopment.aegis.Preferences; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.crypto.KeyStoreHandle; +import com.beemdevelopment.aegis.crypto.KeyStoreHandleException; +import com.beemdevelopment.aegis.helpers.BiometricSlotInitializer; +import com.beemdevelopment.aegis.helpers.BiometricsHelper; +import com.beemdevelopment.aegis.services.NotificationService; +import com.beemdevelopment.aegis.ui.Dialogs; +import com.beemdevelopment.aegis.ui.SlotManagerActivity; +import com.beemdevelopment.aegis.ui.preferences.SwitchPreference; +import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask; +import com.beemdevelopment.aegis.vault.VaultFileCredentials; +import com.beemdevelopment.aegis.vault.VaultManagerException; +import com.beemdevelopment.aegis.vault.slots.BiometricSlot; +import com.beemdevelopment.aegis.vault.slots.PasswordSlot; +import com.beemdevelopment.aegis.vault.slots.Slot; +import com.beemdevelopment.aegis.vault.slots.SlotException; +import com.beemdevelopment.aegis.vault.slots.SlotList; + +import java.util.List; + +import javax.crypto.Cipher; + +import static android.text.TextUtils.isDigitsOnly; + +public class SecurityPreferencesFragment extends PreferencesFragment { + private SwitchPreference _encryptionPreference; + private SwitchPreference _biometricsPreference; + private Preference _autoLockPreference; + private Preference _setPasswordPreference; + private Preference _slotsPreference; + private Preference _passwordReminderPreference; + private SwitchPreferenceCompat _pinKeyboardPreference; + + @Override + public void onResume() { + super.onResume(); + updateEncryptionPreferences(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.preferences_security); + + Preference tapToRevealPreference = findPreference("pref_tap_to_reveal"); + tapToRevealPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRefresh", true); + return true; + }); + + Preference screenPreference = findPreference("pref_secure_screen"); + screenPreference.setOnPreferenceChangeListener((preference, newValue) -> { + getResult().putExtra("needsRecreate", true); + Window window = getActivity().getWindow(); + if ((boolean) newValue) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE); + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE); + } + return true; + }); + + Preference tapToRevealTimePreference = findPreference("pref_tap_to_reveal_time"); + tapToRevealTimePreference.setSummary(getPreferences().getTapToRevealTime() + " seconds"); + tapToRevealTimePreference.setOnPreferenceClickListener(preference -> { + Dialogs.showNumberPickerDialog(getActivity(), number -> { + getPreferences().setTapToRevealTime(number); + tapToRevealTimePreference.setSummary(number + " seconds"); + getResult().putExtra("needsRefresh", true); + }); + return false; + }); + + _encryptionPreference = findPreference("pref_encryption"); + _encryptionPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if (!getVault().isEncryptionEnabled()) { + Dialogs.showSetPasswordDialog(getActivity(), new EnableEncryptionListener()); + } else { + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.disable_encryption) + .setMessage(getString(R.string.disable_encryption_description)) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + try { + getVault().disableEncryption(); + } catch (VaultManagerException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.disable_encryption_error, e); + return; + } + + // clear the KeyStore + try { + KeyStoreHandle handle = new KeyStoreHandle(); + handle.clear(); + } catch (KeyStoreHandleException e) { + e.printStackTrace(); + } + + getActivity().stopService(new Intent(getActivity(), NotificationService.class)); + updateEncryptionPreferences(); + }) + .setNegativeButton(android.R.string.no, null) + .create()); + } + return false; + }); + + _biometricsPreference = findPreference("pref_biometrics"); + _biometricsPreference.setOnPreferenceChangeListener((preference, newValue) -> { + VaultFileCredentials creds = getVault().getCredentials(); + SlotList slots = creds.getSlots(); + + if (!slots.has(BiometricSlot.class)) { + if (BiometricsHelper.isAvailable(getContext())) { + BiometricSlotInitializer initializer = new BiometricSlotInitializer(SecurityPreferencesFragment.this, new RegisterBiometricsListener()); + BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() + .setTitle(getString(R.string.set_up_biometric)) + .setNegativeButtonText(getString(android.R.string.cancel)) + .build(); + initializer.authenticate(info); + } + } else { + // remove the biometric slot + BiometricSlot slot = slots.find(BiometricSlot.class); + slots.remove(slot); + getVault().setCredentials(creds); + + // remove the KeyStore key + try { + KeyStoreHandle handle = new KeyStoreHandle(); + handle.deleteKey(slot.getUUID().toString()); + } catch (KeyStoreHandleException e) { + e.printStackTrace(); + } + + saveVault(); + updateEncryptionPreferences(); + } + + return false; + }); + + _setPasswordPreference = findPreference("pref_password"); + _setPasswordPreference.setOnPreferenceClickListener(preference -> { + Dialogs.showSetPasswordDialog(getActivity(), new SetPasswordListener()); + return false; + }); + + _slotsPreference = findPreference("pref_slots"); + _slotsPreference.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(getActivity(), SlotManagerActivity.class); + intent.putExtra("creds", getVault().getCredentials()); + startActivityForResult(intent, CODE_SLOTS); + return true; + }); + + _pinKeyboardPreference = findPreference("pref_pin_keyboard"); + _pinKeyboardPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if (!(boolean) newValue) { + return true; + } + + Dialogs.showPasswordInputDialog(getActivity(), R.string.set_password_confirm, R.string.pin_keyboard_description, password -> { + if (isDigitsOnly(new String(password))) { + List slots = getVault().getCredentials().getSlots().findAll(PasswordSlot.class); + PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password); + PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(getActivity(), new PasswordConfirmationListener()); + task.execute(getLifecycle(), params); + } else { + _pinKeyboardPreference.setChecked(false); + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.pin_keyboard_error) + .setMessage(R.string.pin_keyboard_error_description) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null) + .create()); + } + }, dialog -> { + _pinKeyboardPreference.setChecked(false); + }); + return false; + }); + + _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] = getPreferences().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]; + } + } + + getPreferences().setAutoLockMask(autoLock); + _autoLockPreference.setSummary(getAutoLockSummary()); + }) + .setNegativeButton(android.R.string.cancel, null); + Dialogs.showSecureDialog(builder.create()); + + return false; + }); + + _passwordReminderPreference = findPreference("pref_password_reminder"); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null && requestCode == CODE_SLOTS) { + onSlotManagerResult(resultCode, data); + } + } + + private void onSlotManagerResult(int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + return; + } + + VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds"); + getVault().setCredentials(creds); + saveVault(); + updateEncryptionPreferences(); + } + + private void updateEncryptionPreferences() { + boolean encrypted = getVault().isEncryptionEnabled(); + _encryptionPreference.setChecked(encrypted, true); + _setPasswordPreference.setVisible(encrypted); + _biometricsPreference.setVisible(encrypted); + _slotsPreference.setEnabled(encrypted); + _autoLockPreference.setVisible(encrypted); + _pinKeyboardPreference.setVisible(encrypted); + + if (encrypted) { + SlotList slots = getVault().getCredentials().getSlots(); + boolean multiPassword = slots.findAll(PasswordSlot.class).size() > 1; + boolean multiBio = slots.findAll(BiometricSlot.class).size() > 1; + boolean showSlots = BuildConfig.DEBUG || multiPassword || multiBio; + boolean canUseBio = BiometricsHelper.isAvailable(getContext()); + _setPasswordPreference.setEnabled(!multiPassword); + _biometricsPreference.setEnabled(canUseBio && !multiBio); + _biometricsPreference.setChecked(slots.has(BiometricSlot.class), true); + _slotsPreference.setVisible(showSlots); + _passwordReminderPreference.setVisible(slots.has(BiometricSlot.class)); + } else { + _setPasswordPreference.setEnabled(false); + _biometricsPreference.setEnabled(false); + _biometricsPreference.setChecked(false, true); + _slotsPreference.setVisible(false); + _passwordReminderPreference.setVisible(false); + } + } + + 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 (getPreferences().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) { + VaultFileCredentials creds = getVault().getCredentials(); + SlotList slots = creds.getSlots(); + + try { + // encrypt the master key for this slot + slot.setKey(creds.getKey(), cipher); + + // remove the old master password slot + PasswordSlot oldSlot = creds.getSlots().find(PasswordSlot.class); + if (oldSlot != null) { + slots.remove(oldSlot); + } + + // add the new master password slot + slots.add(slot); + } catch (SlotException e) { + onException(e); + return; + } + + getVault().setCredentials(creds); + saveVault(); + + if (getPreferences().isPinKeyboardEnabled()) { + _pinKeyboardPreference.setChecked(false); + Toast.makeText(getContext(), R.string.pin_keyboard_disabled, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onException(Exception e) { + e.printStackTrace(); + updateEncryptionPreferences(); + Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e); + } + } + + private class RegisterBiometricsListener implements BiometricSlotInitializer.Listener { + @Override + public void onInitializeSlot(BiometricSlot slot, Cipher cipher) { + VaultFileCredentials creds = getVault().getCredentials(); + try { + slot.setKey(creds.getKey(), cipher); + } catch (SlotException e) { + e.printStackTrace(); + onSlotInitializationFailed(0, e.toString()); + return; + } + creds.getSlots().add(slot); + getVault().setCredentials(creds); + + saveVault(); + updateEncryptionPreferences(); + } + + @Override + public void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString) { + if (!BiometricsHelper.isCanceled(errorCode)) { + Dialogs.showErrorDialog(getContext(), R.string.encryption_enable_biometrics_error, errString); + } + } + } + + private class EnableEncryptionListener implements Dialogs.SlotListener { + @Override + public void onSlotResult(Slot slot, Cipher cipher) { + VaultFileCredentials creds = new VaultFileCredentials(); + + try { + slot.setKey(creds.getKey(), cipher); + creds.getSlots().add(slot); + getVault().enableEncryption(creds); + } catch (VaultManagerException | SlotException e) { + onException(e); + return; + } + + getActivity().startService(new Intent(getActivity(), NotificationService.class)); + updateEncryptionPreferences(); + } + + @Override + public void onException(Exception e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.encryption_set_password_error, e); + } + } + + private class PasswordConfirmationListener implements PasswordSlotDecryptTask.Callback { + @Override + public void onTaskFinished(PasswordSlotDecryptTask.Result result) { + if (result != null) { + _pinKeyboardPreference.setChecked(true); + } else { + Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity()) + .setTitle(R.string.pin_keyboard_error) + .setMessage(R.string.invalid_password) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null) + .create()); + _pinKeyboardPreference.setChecked(false); + } + } + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/ToolsPreferencesFragment.java b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/ToolsPreferencesFragment.java new file mode 100644 index 00000000..a1877cfd --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/fragments/ToolsPreferencesFragment.java @@ -0,0 +1,485 @@ +package com.beemdevelopment.aegis.ui.fragments; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.FileProvider; +import androidx.preference.Preference; + +import com.beemdevelopment.aegis.BuildConfig; +import com.beemdevelopment.aegis.R; +import com.beemdevelopment.aegis.helpers.SpinnerHelper; +import com.beemdevelopment.aegis.importers.AegisImporter; +import com.beemdevelopment.aegis.importers.DatabaseImporter; +import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException; +import com.beemdevelopment.aegis.importers.DatabaseImporterException; +import com.beemdevelopment.aegis.ui.AuthActivity; +import com.beemdevelopment.aegis.ui.Dialogs; +import com.beemdevelopment.aegis.ui.SelectEntriesActivity; +import com.beemdevelopment.aegis.ui.models.ImportEntry; +import com.beemdevelopment.aegis.ui.tasks.ExportTask; +import com.beemdevelopment.aegis.util.UUIDMap; +import com.beemdevelopment.aegis.vault.VaultBackupManager; +import com.beemdevelopment.aegis.vault.VaultEntry; +import com.beemdevelopment.aegis.vault.VaultFileCredentials; +import com.beemdevelopment.aegis.vault.VaultManager; +import com.beemdevelopment.aegis.vault.VaultManagerException; +import com.beemdevelopment.aegis.vault.slots.Slot; +import com.beemdevelopment.aegis.vault.slots.SlotException; +import com.topjohnwu.superuser.Shell; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.crypto.Cipher; + +public class ToolsPreferencesFragment extends PreferencesFragment { + // keep a reference to the type of database converter the user selected + private Class _importerType; + private AegisImporter.State _importerState; + private UUIDMap _importerEntries; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.preferences_tools); + + Preference importPreference = findPreference("pref_import"); + importPreference.setOnPreferenceClickListener(preference -> { + Dialogs.showImportersDialog(getContext(), false, definition -> { + _importerType = definition.getType(); + + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + startActivityForResult(intent, CODE_IMPORT); + }); + return true; + }); + + Preference importAppPreference = findPreference("pref_import_app"); + importAppPreference.setOnPreferenceClickListener(preference -> { + Dialogs.showImportersDialog(getContext(), true, definition -> { + DatabaseImporter importer = DatabaseImporter.create(getContext(), definition.getType()); + importApp(importer); + }); + return true; + }); + + Preference exportPreference = findPreference("pref_export"); + exportPreference.setOnPreferenceClickListener(preference -> { + startExport(); + return true; + }); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null) { + switch (requestCode) { + case CODE_IMPORT: + onImportResult(resultCode, data); + break; + case CODE_IMPORT_DECRYPT: + onImportDecryptResult(resultCode, data); + break; + case CODE_SELECT_ENTRIES: + onSelectEntriesResult(resultCode, data); + break; + case CODE_EXPORT: + // intentional fallthrough + case CODE_EXPORT_PLAIN: + // intentional fallthrough + case CODE_EXPORT_GOOGLE_URI: + onExportResult(requestCode, resultCode, data); + break; + } + } + } + + private void importApp(DatabaseImporter importer) { + // obtain the global root shell and close it immediately after we're done + // TODO: find a way to use SuFileInputStream with Shell.newInstance() + try (Shell shell = Shell.getShell()) { + if (!shell.isRoot()) { + Toast.makeText(getActivity(), R.string.root_error, Toast.LENGTH_SHORT).show(); + return; + } + + DatabaseImporter.State state = importer.readFromApp(); + processImporterState(state); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + Toast.makeText(getActivity(), R.string.app_lookup_error, Toast.LENGTH_SHORT).show(); + } catch (IOException | DatabaseImporterException e) { + e.printStackTrace(); + Toast.makeText(getActivity(), R.string.reading_file_error, Toast.LENGTH_SHORT).show(); + } + } + + private void processImporterState(DatabaseImporter.State state) { + try { + if (state.isEncrypted()) { + // temporary special case for encrypted Aegis vaults + if (state instanceof AegisImporter.EncryptedState) { + _importerState = state; + + Intent intent = new Intent(getActivity(), AuthActivity.class); + intent.putExtra("slots", ((AegisImporter.EncryptedState) state).getSlots()); + startActivityForResult(intent, CODE_IMPORT_DECRYPT); + } else { + state.decrypt(getActivity(), new DatabaseImporter.DecryptListener() { + @Override + public void onStateDecrypted(DatabaseImporter.State state) { + importDatabase(state); + } + + @Override + public void onError(Exception e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e); + } + }); + } + } else { + importDatabase(state); + } + } catch (DatabaseImporterException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e); + } + } + + private void onImportDecryptResult(int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + _importerState = null; + return; + } + + VaultFileCredentials creds = (VaultFileCredentials) data.getSerializableExtra("creds"); + DatabaseImporter.State state; + try { + state = ((AegisImporter.EncryptedState) _importerState).decrypt(creds); + } catch (DatabaseImporterException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.decryption_error, e); + return; + } + + importDatabase(state); + _importerState = null; + } + + private void onImportResult(int resultCode, Intent data) { + Uri uri = data.getData(); + if (resultCode != Activity.RESULT_OK || uri == null) { + return; + } + + try (InputStream stream = getContext().getContentResolver().openInputStream(uri)) { + DatabaseImporter importer = DatabaseImporter.create(getContext(), _importerType); + DatabaseImporter.State state = importer.read(stream); + processImporterState(state); + } catch (FileNotFoundException e) { + Toast.makeText(getActivity(), R.string.file_not_found, Toast.LENGTH_SHORT).show(); + } catch (DatabaseImporterException | IOException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.reading_file_error, e); + } + } + + private void importDatabase(DatabaseImporter.State state) { + DatabaseImporter.Result result; + try { + result = state.convert(); + } catch (DatabaseImporterException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.parsing_file_error, e); + return; + } + + _importerEntries = result.getEntries(); + List entries = new ArrayList<>(); + for (VaultEntry entry : _importerEntries) { + entries.add(new ImportEntry(entry)); + } + + Intent intent = new Intent(getActivity(), SelectEntriesActivity.class); + intent.putExtra("entries", (ArrayList) entries); + intent.putExtra("errors", (ArrayList) result.getErrors()); + intent.putExtra("vaultContainsEntries", getVault().getEntries().size() > 0); + startActivityForResult(intent, CODE_SELECT_ENTRIES); + } + + private void startExport() { + View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_export, null); + TextView warningText = view.findViewById(R.id.text_export_warning); + CheckBox checkBoxEncrypt = view.findViewById(R.id.checkbox_export_encrypt); + CheckBox checkBoxAccept = view.findViewById(R.id.checkbox_accept); + Spinner spinner = view.findViewById(R.id.spinner_export_format); + SpinnerHelper.fillSpinner(getContext(), spinner, R.array.export_formats); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + checkBoxEncrypt.setChecked(position == 0); + checkBoxEncrypt.setEnabled(position == 0); + warningText.setVisibility(checkBoxEncrypt.isChecked() ? View.GONE : View.VISIBLE); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setTitle(R.string.pref_export_summary) + .setView(view) + .setNeutralButton(R.string.share, null) + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel, null) + .create(); + + dialog.setOnShowListener(d -> { + Button btnPos = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + Button btnNeutral = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + + checkBoxEncrypt.setOnCheckedChangeListener((buttonView, isChecked) -> { + warningText.setVisibility(isChecked ? View.GONE : View.VISIBLE); + checkBoxAccept.setVisibility(isChecked ? View.GONE : View.VISIBLE); + checkBoxAccept.setChecked(false); + btnPos.setEnabled(isChecked); + btnNeutral.setEnabled(isChecked); + }); + + checkBoxAccept.setOnCheckedChangeListener((buttonView, isChecked) -> { + btnPos.setEnabled(isChecked); + btnNeutral.setEnabled(isChecked); + }); + + btnPos.setOnClickListener(v -> { + dialog.dismiss(); + + if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) { + return; + } + + int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); + VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(getExportMimeType(requestCode)) + .putExtra(Intent.EXTRA_TITLE, fileInfo.toString()); + startActivityForResult(intent, requestCode); + }); + + btnNeutral.setOnClickListener(v -> { + dialog.dismiss(); + + if (!checkBoxEncrypt.isChecked() && !checkBoxAccept.isChecked()) { + return; + } + + File file; + try { + VaultBackupManager.FileInfo fileInfo = getExportFileInfo(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); + file = File.createTempFile(fileInfo.getFilename() + "-", "." + fileInfo.getExtension(), getExportCacheDir()); + } catch (IOException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); + return; + } + + int requestCode = getExportRequestCode(spinner.getSelectedItemPosition(), checkBoxEncrypt.isChecked()); + startExportVault(requestCode, cb -> { + try (OutputStream stream = new FileOutputStream(file)) { + cb.exportVault(stream); + } catch (IOException | VaultManagerException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); + return; + } + + Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.FILE_PROVIDER_AUTHORITY, file); + Intent intent = new Intent(Intent.ACTION_SEND) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setType(getExportMimeType(requestCode)) + .putExtra(Intent.EXTRA_STREAM, uri); + Intent chooser = Intent.createChooser(intent, getString(R.string.pref_export_summary)); + startActivity(chooser); + }); + }); + }); + + Dialogs.showSecureDialog(dialog); + } + + private void onSelectEntriesResult(int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + return; + } + + boolean wipeEntries = data.getBooleanExtra("wipeEntries", false); + if (wipeEntries) { + getVault().wipeEntries(); + } + + List selectedEntries = (ArrayList) data.getSerializableExtra("entries"); + for (ImportEntry selectedEntry : selectedEntries) { + VaultEntry savedEntry = _importerEntries.getByUUID(selectedEntry.getUUID()); + + // temporary: randomize the UUID of duplicate entries and add them anyway + if (getVault().isEntryDuplicate(savedEntry)) { + savedEntry.resetUUID(); + } + + getVault().addEntry(savedEntry); + } + + _importerEntries = null; + if (!saveVault()) { + return; + } + + String toastMessage = getResources().getQuantityString(R.plurals.imported_entries_count, selectedEntries.size(), selectedEntries.size()); + Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show(); + + getResult().putExtra("needsRecreate", true); + } + + private static int getExportRequestCode(int spinnerPos, boolean encrypt) { + if (spinnerPos == 0) { + return encrypt ? CODE_EXPORT : CODE_EXPORT_PLAIN; + } + + return CODE_EXPORT_GOOGLE_URI; + } + + private static VaultBackupManager.FileInfo getExportFileInfo(int spinnerPos, boolean encrypt) { + if (spinnerPos == 0) { + String filename = encrypt ? VaultManager.FILENAME_PREFIX_EXPORT : VaultManager.FILENAME_PREFIX_EXPORT_PLAIN; + return new VaultBackupManager.FileInfo(filename); + } + + return new VaultBackupManager.FileInfo(VaultManager.FILENAME_PREFIX_EXPORT_URI, "txt"); + } + + private static String getExportMimeType(int requestCode) { + return requestCode == CODE_EXPORT_GOOGLE_URI ? "text/plain" : "application/json"; + } + + private File getExportCacheDir() throws IOException { + File dir = new File(getContext().getCacheDir(), "export"); + if (!dir.exists() && !dir.mkdir()) { + throw new IOException(String.format("Unable to create directory %s", dir)); + } + + return dir; + } + + private void startExportVault(int requestCode, StartExportCallback cb) { + switch (requestCode) { + case CODE_EXPORT: + if (getVault().isEncryptionEnabled()) { + cb.exportVault(stream -> getVault().export(stream)); + } else { + Dialogs.showSetPasswordDialog(getActivity(), new Dialogs.SlotListener() { + @Override + public void onSlotResult(Slot slot, Cipher cipher) { + VaultFileCredentials creds = new VaultFileCredentials(); + + try { + slot.setKey(creds.getKey(), cipher); + creds.getSlots().add(slot); + } catch (SlotException e) { + onException(e); + return; + } + + cb.exportVault(stream -> getVault().export(stream, creds)); + } + + @Override + public void onException(Exception e) { + + } + }); + } + break; + case CODE_EXPORT_PLAIN: + cb.exportVault((stream) -> getVault().export(stream, null)); + break; + case CODE_EXPORT_GOOGLE_URI: + cb.exportVault((stream) -> getVault().exportGoogleUris(stream)); + break; + } + } + + private void onExportResult(int requestCode, int resultCode, Intent data) { + Uri uri = data.getData(); + if (resultCode != Activity.RESULT_OK || uri == null) { + return; + } + + + startExportVault(requestCode, cb -> { + File file; + OutputStream outStream = null; + try { + file = File.createTempFile(VaultManager.FILENAME_PREFIX_EXPORT + "-", ".json", getExportCacheDir()); + outStream = new FileOutputStream(file); + cb.exportVault(outStream); + + new ExportTask(getContext(), new ExportResultListener()).execute(getLifecycle(), new ExportTask.Params(file, uri)); + } catch (VaultManagerException | IOException e) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); + } finally { + try { + if (outStream != null) { + outStream.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + private class ExportResultListener implements ExportTask.Callback { + @Override + public void onTaskFinished(Exception e) { + if (e != null) { + e.printStackTrace(); + Dialogs.showErrorDialog(getContext(), R.string.exporting_vault_error, e); + } else { + Toast.makeText(getContext(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show(); + } + } + } + + private interface FinishExportCallback { + void exportVault(OutputStream stream) throws IOException, VaultManagerException; + } + + private interface StartExportCallback { + void exportVault(FinishExportCallback exportCb); + } +} diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 00000000..0ae44c05 --- /dev/null +++ b/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 00000000..cc3919c9 --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 00000000..f304d1a5 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 00000000..448f510b --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_brush_black_24dp.xml b/app/src/main/res/drawable/ic_brush_black_24dp.xml new file mode 100644 index 00000000..913e34cf --- /dev/null +++ b/app/src/main/res/drawable/ic_brush_black_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cloud_upload_outline_black_24dp.xml b/app/src/main/res/drawable/ic_cloud_upload_outline_black_24dp.xml new file mode 100644 index 00000000..50f2f687 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_upload_outline_black_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_gesture_tap_24dp.xml b/app/src/main/res/drawable/ic_gesture_tap_24dp.xml new file mode 100644 index 00000000..0bd74d8b --- /dev/null +++ b/app/src/main/res/drawable/ic_gesture_tap_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tools_black_24dp.xml b/app/src/main/res/drawable/ic_tools_black_24dp.xml new file mode 100644 index 00000000..42af86dd --- /dev/null +++ b/app/src/main/res/drawable/ic_tools_black_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar-rSA/strings.xml b/app/src/main/res/values-ar-rSA/strings.xml index eeedcfe6..514a32e5 100644 --- a/app/src/main/res/values-ar-rSA/strings.xml +++ b/app/src/main/res/values-ar-rSA/strings.xml @@ -12,7 +12,6 @@ المصدِّر التفضيلات المظهر - العامة الأمان الأدوات النسخ الاحتياطي diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 1340a563..2da03b5e 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -12,7 +12,6 @@ Poskytovatel Předvolby Vzhled - Obecné Zabezpečení Nástroje Zálohy diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 98730879..60288308 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -12,7 +12,6 @@ Herausgeber Einstellungen Erscheinungsbild - Allgemein Sicherheit Werkzeuge Sicherungskopien diff --git a/app/src/main/res/values-el-rGR/strings.xml b/app/src/main/res/values-el-rGR/strings.xml index 40d519aa..9df6d883 100644 --- a/app/src/main/res/values-el-rGR/strings.xml +++ b/app/src/main/res/values-el-rGR/strings.xml @@ -12,7 +12,6 @@ Εκδότης Προτιμήσεις Εμφάνιση - Γενικά Ασφάλεια Εργαλεία Αντίγραφα Ασφαλείας diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 214a0abe..5cc9835f 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -12,7 +12,6 @@ Emisor Preferencias Apariencia - General Seguridad Herramientas Copias de seguridad diff --git a/app/src/main/res/values-eu-rES/strings.xml b/app/src/main/res/values-eu-rES/strings.xml index 00149fb4..a0380aac 100644 --- a/app/src/main/res/values-eu-rES/strings.xml +++ b/app/src/main/res/values-eu-rES/strings.xml @@ -12,7 +12,6 @@ Igorlea Hobespenak Itxura - Orokorra Segurtasuna Tresnak Segurtasun-kopiak diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index 3717a657..2e319515 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -12,7 +12,6 @@ صادر کننده تنظیمات ظاهر - عمومی امنیت ابزارها پشتیبان گیری diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 9e7882fd..15df85e2 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -12,7 +12,6 @@ Myöntäjä Asetukset Ulkoasu - Yleiset Turvallisuus Työkalut Varmuuskopiot diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 7b106f31..968cfd5e 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -12,7 +12,6 @@ Émetteur Préférences Apparence - Général Sécurité Outils Sauvegardes diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 9d9ac240..4b60cd67 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -12,7 +12,6 @@ ज़ारीकर्ता वरीयताएँ रंगरूप - सामान्य सुरक्षा टूल्स बैकअप्स diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index e5bb17cf..43abc48a 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -10,7 +10,6 @@ Kibocsátó Beállítások Megjelenés - Általános Biztonság Eszközök Biztonsági mentések diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 281b3c32..72e7a934 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -12,7 +12,6 @@ Penerbit Pilihan Tampilan - Umum Keamanan Alat Cadangan diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index 6108b6d8..6bd7bb96 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -12,7 +12,6 @@ Emittente Impostazioni Aspetto - Generale Sicurezza Strumenti Backup diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index e3aaaa22..89e3691e 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -12,7 +12,6 @@ 発行者 設定 外観 - 全般 セキュリティ ツール バックアップ diff --git a/app/src/main/res/values-kn-rIN/strings.xml b/app/src/main/res/values-kn-rIN/strings.xml index 94cb6e1e..2b528003 100644 --- a/app/src/main/res/values-kn-rIN/strings.xml +++ b/app/src/main/res/values-kn-rIN/strings.xml @@ -10,7 +10,6 @@ ನೀಡುವವರು ಸಿದ್ಢತೆಗಳು ನೋಟ - ಸಾಮಾನ್ಯ ಭದ್ರತೆ ಉಪಕರಣಗಳು ಥೀಮ್ diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 931a7315..0c92634d 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -12,7 +12,6 @@ Uitgever Voorkeuren Uiterlijk - Algemeen Beveiliging Hulpmiddelen Back-ups diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 666f02c5..18047ce6 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -12,7 +12,6 @@ Wydawca Ustawienia Wygląd - Ogólne Bezpieczeństwo Narzędzia Kopie zapasowe diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 84b68390..29e35b32 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -12,7 +12,6 @@ Nome do serviço Preferências Aparência - Geral Segurança Ferramentas Backups diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index dec4463f..1a3153a1 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -12,7 +12,6 @@ Emissor Preferências Aparência - Geral Segurança Ferramentas Backups diff --git a/app/src/main/res/values-ro-rRO/strings.xml b/app/src/main/res/values-ro-rRO/strings.xml index e2a62d7c..d96aecb4 100644 --- a/app/src/main/res/values-ro-rRO/strings.xml +++ b/app/src/main/res/values-ro-rRO/strings.xml @@ -12,7 +12,6 @@ Emitent Preferințe Aspect - General Securitate Unelte Copii de rezervă diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index d0016a0d..fefc7bb3 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -12,7 +12,6 @@ Эмитент Предпочтения Внешний вид - Общее Безопасность Инструменты Резервные копии diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 0a7043fa..ca53eb04 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -12,7 +12,6 @@ Vydavateľ Predvoľby Vzhľad - Všeobecné Zabezpečenie Nástroje Zálohy diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index e1b9eadb..4efbe37f 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -10,7 +10,6 @@ Sağlayıcı Ayarlar Görünüm - Genel Güvenlik Araçlar Yedekler diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 05441de5..92aadc12 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -12,7 +12,6 @@ 服务商 偏好设置 外观 - 常规 安全 工具 备份 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9e7d7134..5b30f644 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -12,7 +12,6 @@ 服務商 偏好設定 外觀 - 一般 安全 工具 備份 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3907560..584180ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,8 +15,8 @@ Issuer Preferences + Behavior Appearance - General Security Tools Backups diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e186b3a8..bb79872b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -22,6 +22,7 @@ @color/code_primary_text @color/colorPrimary @color/colorPrimaryDark + @style/PreferenceTheme @color/colorAccent @color/background @color/divider @@ -30,7 +31,7 @@ @color/icon_primary_inverted @style/ActionModeStyle - @style/ThemeOverlay.AppCompat.Dark.ActionBar + @style/ActionBarTheme @style/DialogStyle @color/indicator_dot @@ -158,6 +159,7 @@ @color/code_primary_text_dark @color/colorPrimary @color/colorPrimaryDark + @style/PreferenceTheme @color/colorAccent @color/background_dark @color/divider_dark @@ -181,7 +183,7 @@ @color/background_true_dark @color/card_background_focused_true_dark @color/background_true_dark - @style/PreferenceThemeOverlay.v14.Material + @style/PreferenceTheme @style/ActionModeStyle.TrueBlack @style/DialogStyle.TrueDark @color/divider_true_dark @@ -230,4 +232,11 @@ + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 91e20e7b..1dec9c13 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -2,199 +2,31 @@ + android:title="@string/action_settings"> - - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/xml/preferences_appearance.xml b/app/src/main/res/xml/preferences_appearance.xml new file mode 100644 index 00000000..e7959577 --- /dev/null +++ b/app/src/main/res/xml/preferences_appearance.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences_backups.xml b/app/src/main/res/xml/preferences_backups.xml new file mode 100644 index 00000000..2f418d0c --- /dev/null +++ b/app/src/main/res/xml/preferences_backups.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_behavior.xml b/app/src/main/res/xml/preferences_behavior.xml new file mode 100644 index 00000000..9633b72c --- /dev/null +++ b/app/src/main/res/xml/preferences_behavior.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/xml/preferences_security.xml b/app/src/main/res/xml/preferences_security.xml new file mode 100644 index 00000000..f5524a5b --- /dev/null +++ b/app/src/main/res/xml/preferences_security.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_tools.xml b/app/src/main/res/xml/preferences_tools.xml new file mode 100644 index 00000000..8a710f7b --- /dev/null +++ b/app/src/main/res/xml/preferences_tools.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file