Split preferences into separate fragments

<img src="https://alexbakker.me/u/5phth0i1ga.png" width="300" />
This commit is contained in:
Alexander Bakker 2021-01-13 21:03:14 +01:00
parent 260a3b9c78
commit ef8c116bdc
53 changed files with 1706 additions and 1406 deletions

View file

@ -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<? extends PreferencesFragment> 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: {

View file

@ -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<? extends PreferencesFragment> fragmentType = (Class<? extends PreferencesFragment>) getIntent().getSerializableExtra("fragment");
if (fragmentType == null) {
return null;
}
try {
return fragmentType.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -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<String> groups = new HashSet<>(data.getStringArrayListExtra("groups"));
for (VaultEntry entry : getVault().getEntries()) {
if (!groups.contains(entry.getGroup())) {
entry.setGroup(null);
}
}
saveVault();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<? extends DatabaseImporter> _importerType;
private AegisImporter.State _importerState;
private UUIDMap<VaultEntry> _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<ImportEntry> entries = new ArrayList<>();
for (VaultEntry entry : _importerEntries) {
entries.add(new ImportEntry(entry));
}
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
intent.putExtra("entries", (ArrayList<ImportEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) 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<ImportEntry> selectedEntries = (ArrayList<ImportEntry>) 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);
}
}