mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-23 07:19:13 +00:00
Merge pull request #341 from alexbakker/backups
Add support for backups
This commit is contained in:
commit
c578543e18
13 changed files with 475 additions and 46 deletions
|
@ -17,7 +17,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "com.beemdevelopment.aegis"
|
||||
minSdkVersion 19
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 29
|
||||
versionName "1.1.4"
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.beemdevelopment.aegis;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
|
@ -121,4 +122,33 @@ public class Preferences {
|
|||
|
||||
return new Locale(lang);
|
||||
}
|
||||
|
||||
public boolean isBackupsEnabled() {
|
||||
return _prefs.getBoolean("pref_backups", false);
|
||||
}
|
||||
|
||||
public void setIsBackupsEnabled(boolean enabled) {
|
||||
_prefs.edit().putBoolean("pref_backups", enabled).apply();
|
||||
}
|
||||
|
||||
public Uri getBackupsLocation() {
|
||||
String str = _prefs.getString("pref_backups_location", null);
|
||||
if (str != null) {
|
||||
return Uri.parse(str);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBackupsLocation(Uri location) {
|
||||
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
|
||||
}
|
||||
|
||||
public int getBackupsVersionCount() {
|
||||
return _prefs.getInt("pref_backups_versions", 5);
|
||||
}
|
||||
|
||||
public void setBackupsVersionCount(int versions) {
|
||||
_prefs.edit().putInt("pref_backups_versions", versions).apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context;
|
|||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.util.UUIDMap;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
|
@ -151,9 +151,7 @@ public abstract class DatabaseImporter {
|
|||
}
|
||||
|
||||
public byte[] readAll() throws IOException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(_stream)) {
|
||||
return stream.getBytes();
|
||||
}
|
||||
return IOUtils.readAll(_stream);
|
||||
}
|
||||
|
||||
public InputStream getStream() {
|
||||
|
|
|
@ -3,12 +3,11 @@ package com.beemdevelopment.aegis.importers;
|
|||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.encoding.Base64;
|
||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
@ -45,8 +44,9 @@ public class SteamImporter extends DatabaseImporter {
|
|||
|
||||
@Override
|
||||
public State read(FileReader reader) throws DatabaseImporterException {
|
||||
try (ByteInputStream stream = ByteInputStream.create(reader.getStream())) {
|
||||
JSONObject obj = new JSONObject(new String(stream.getBytes(), StandardCharsets.UTF_8));
|
||||
try {
|
||||
byte[] bytes = reader.readAll();
|
||||
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
||||
return new State(obj);
|
||||
} catch (IOException | JSONException e) {
|
||||
throw new DatabaseImporterException(e);
|
||||
|
|
|
@ -214,6 +214,31 @@ public class Dialogs {
|
|||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public static void showBackupVersionsPickerDialog(Activity activity, NumberInputListener listener) {
|
||||
final int max = 30;
|
||||
String[] numbers = new String[max / 5];
|
||||
for (int i = 0; i < numbers.length; i++) {
|
||||
numbers[i] = Integer.toString(i * 5 + 5);
|
||||
}
|
||||
|
||||
View view = activity.getLayoutInflater().inflate(R.layout.dialog_number_picker, null);
|
||||
NumberPicker numberPicker = view.findViewById(R.id.numberPicker);
|
||||
numberPicker.setDisplayedValues(numbers);
|
||||
numberPicker.setMaxValue(numbers.length - 1);
|
||||
numberPicker.setMinValue(0);
|
||||
numberPicker.setValue(new Preferences(activity.getApplicationContext()).getBackupsVersionCount() / 5 - 1);
|
||||
numberPicker.setWrapSelectorWheel(false);
|
||||
|
||||
AlertDialog dialog = new AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.set_number)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog1, which) ->
|
||||
listener.onNumberInputResult(numberPicker.getValue()))
|
||||
.create();
|
||||
|
||||
showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
public interface NumberInputListener {
|
||||
void onNumberInputResult(int number);
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@ import androidx.appcompat.app.AlertDialog;
|
|||
import androidx.biometric.BiometricPrompt;
|
||||
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.CancelAction;
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.Theme;
|
||||
import com.beemdevelopment.aegis.ViewMode;
|
||||
|
@ -71,8 +73,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
private static final int CODE_SELECT_ENTRIES = 4;
|
||||
private static final int CODE_EXPORT = 5;
|
||||
private static final int CODE_EXPORT_ENCRYPT = 6;
|
||||
private static final int CODE_BACKUPS = 7;
|
||||
|
||||
private Intent _result;
|
||||
private Preferences _prefs;
|
||||
private VaultManager _vault;
|
||||
|
||||
// keep a reference to the type of database converter the user selected
|
||||
|
@ -87,30 +91,35 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
private Preference _slotsPreference;
|
||||
private Preference _groupsPreference;
|
||||
private Preference _passwordReminderPreference;
|
||||
private SwitchPreferenceCompat _backupsPreference;
|
||||
private Preference _backupsLocationPreference;
|
||||
private Preference _backupsTriggerPreference;
|
||||
private Preference _backupsVersionsPreference;
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
AegisApplication app = (AegisApplication) getActivity().getApplication();
|
||||
_prefs = app.getPreferences();
|
||||
_vault = app.getVaultManager();
|
||||
|
||||
// set the result intent in advance
|
||||
setResult(new Intent());
|
||||
|
||||
int currentTheme = app.getPreferences().getCurrentTheme().ordinal();
|
||||
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 = app.getPreferences().getCurrentTheme().ordinal();
|
||||
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();
|
||||
app.getPreferences().setCurrentTheme(Theme.fromInteger(i));
|
||||
_prefs.setCurrentTheme(Theme.fromInteger(i));
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
|
@ -136,19 +145,19 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
langPreference.setVisible(false);
|
||||
}
|
||||
|
||||
int currentViewMode = app.getPreferences().getCurrentViewMode().ordinal();
|
||||
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 = app.getPreferences().getCurrentViewMode().ordinal();
|
||||
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();
|
||||
app.getPreferences().setCurrentViewMode(ViewMode.fromInteger(i));
|
||||
_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();
|
||||
|
@ -360,6 +369,51 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
}
|
||||
});
|
||||
|
||||
_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();
|
||||
} catch (VaultManagerException e) {
|
||||
Toast.makeText(getContext(), String.format("Error creating backup: %s", e.getMessage()), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
_backupsVersionsPreference = findPreference("pref_backups_versions");
|
||||
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), _prefs.getBackupsVersionCount()));
|
||||
_backupsVersionsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Dialogs.showBackupVersionsPickerDialog(getActivity(), number -> {
|
||||
number = number * 5 + 5;
|
||||
_prefs.setBackupsVersionCount(number);
|
||||
_backupsVersionsPreference.setSummary(String.format(getString(R.string.pref_backups_versions_summary), number));
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
_autoLockPreference = findPreference("pref_auto_lock");
|
||||
_passwordReminderPreference = findPreference("pref_password_reminder");
|
||||
}
|
||||
|
@ -368,6 +422,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
updateEncryptionPreferences();
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -396,6 +451,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
// intentional fallthrough
|
||||
case CODE_EXPORT_ENCRYPT:
|
||||
onExportResult(resultCode, data, requestCode == CODE_EXPORT_ENCRYPT);
|
||||
case CODE_BACKUPS:
|
||||
onSelectBackupsLocationResult(resultCode, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -662,6 +719,21 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
Toast.makeText(getActivity(), getString(R.string.exported_vault), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
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);
|
||||
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
|
||||
updateBackupPreference();
|
||||
}
|
||||
|
||||
private boolean saveVault() {
|
||||
try {
|
||||
_vault.save();
|
||||
|
@ -699,6 +771,28 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
|||
_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 class SetPasswordListener implements Dialogs.SlotListener {
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ByteInputStream extends ByteArrayInputStream {
|
||||
private ByteInputStream(byte[] buf) {
|
||||
super(buf);
|
||||
}
|
||||
|
||||
public static ByteInputStream create(InputStream fileStream) throws IOException {
|
||||
int read;
|
||||
byte[] buf = new byte[4096];
|
||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||
while ((read = fileStream.read(buf, 0, buf.length)) != -1) {
|
||||
outStream.write(buf, 0, read);
|
||||
}
|
||||
|
||||
return new ByteInputStream(outStream.toByteArray());
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return this.buf;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.beemdevelopment.aegis.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class IOUtils {
|
||||
private IOUtils() {
|
||||
|
||||
}
|
||||
|
||||
public static byte[] readFile(FileInputStream inStream) throws IOException {
|
||||
try (DataInputStream outStream = new DataInputStream(inStream)) {
|
||||
byte[] fileBytes = new byte[(int) inStream.getChannel().size()];
|
||||
outStream.readFully(fileBytes);
|
||||
return fileBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readAll(InputStream inStream) throws IOException {
|
||||
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
|
||||
copy(inStream, outStream);
|
||||
return outStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static void copy(InputStream inStream, OutputStream outStream) throws IOException {
|
||||
int read;
|
||||
byte[] buf = new byte[4096];
|
||||
while ((read = inStream.read(buf, 0, buf.length)) != -1) {
|
||||
outStream.write(buf, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package com.beemdevelopment.aegis.vault;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.UriPermission;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.ParsePosition;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class VaultBackupManager {
|
||||
private static final String TAG = VaultBackupManager.class.getSimpleName();
|
||||
|
||||
private static final StrictDateFormat _dateFormat =
|
||||
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
|
||||
|
||||
public static final String FILENAME_PREFIX = "aegis-backup";
|
||||
|
||||
private Context _context;
|
||||
|
||||
public VaultBackupManager(Context context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public void create(Uri dirUri, int versionsToKeep) throws VaultManagerException {
|
||||
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX, Calendar.getInstance().getTime());
|
||||
DocumentFile dir = DocumentFile.fromTreeUri(_context, dirUri);
|
||||
|
||||
Log.i(TAG, String.format("Creating backup at %s: %s", Uri.decode(dir.getUri().toString()), fileInfo.toString()));
|
||||
|
||||
if (!hasPermissionsAt(dirUri)) {
|
||||
Log.e(TAG, "Unable to create file for backup, no persisted URI permissions");
|
||||
throw new VaultManagerException("No persisted URI permissions");
|
||||
}
|
||||
|
||||
// If we create a file with a name that already exists, SAF will append a number
|
||||
// to the filename and write to that instead. We can't overwrite existing files, so
|
||||
// just avoid that altogether by checking beforehand.
|
||||
if (dir.findFile(fileInfo.toString()) != null) {
|
||||
throw new VaultManagerException("Backup file already exists");
|
||||
}
|
||||
|
||||
DocumentFile file = dir.createFile("application/json", fileInfo.toString());
|
||||
if (file == null) {
|
||||
Log.e(TAG, "Unable to create file for backup, createFile returned null");
|
||||
throw new VaultManagerException("createFile returned null");
|
||||
}
|
||||
|
||||
try (FileInputStream inStream = _context.openFileInput(VaultManager.FILENAME);
|
||||
OutputStream outStream = _context.getContentResolver().openOutputStream(file.getUri())) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to create backup", e);
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
||||
enforceVersioning(dir, versionsToKeep);
|
||||
}
|
||||
|
||||
public boolean hasPermissionsAt(Uri uri) {
|
||||
for (UriPermission perm : _context.getContentResolver().getPersistedUriPermissions()) {
|
||||
if (perm.getUri().equals(uri)) {
|
||||
return perm.isReadPermission() && perm.isWritePermission();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void enforceVersioning(DocumentFile dir, int versionsToKeep) {
|
||||
Log.i(TAG, String.format("Scanning directory %s for backup files", Uri.decode(dir.getUri().toString())));
|
||||
|
||||
List<File> files = new ArrayList<>();
|
||||
for (DocumentFile docFile : dir.listFiles()) {
|
||||
if (docFile.isFile() && !docFile.isVirtual()) {
|
||||
try {
|
||||
files.add(new File(docFile));
|
||||
} catch (ParseException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(files, new FileComparator());
|
||||
for (File file : files) {
|
||||
Log.i(TAG, file.getFile().getName());
|
||||
}
|
||||
|
||||
if (files.size() > versionsToKeep) {
|
||||
for (File file : files.subList(0, files.size() - versionsToKeep)) {
|
||||
Log.i(TAG, String.format("Deleting %s", file.getFile().getName()));
|
||||
if (!file.getFile().delete()) {
|
||||
Log.e(TAG, String.format("Unable to delete %s", file.getFile().getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileInfo {
|
||||
private String _filename;
|
||||
private Date _date;
|
||||
|
||||
public FileInfo(String filename, Date date) {
|
||||
_filename = filename;
|
||||
_date = date;
|
||||
}
|
||||
|
||||
public static FileInfo parseFilename(String filename) throws ParseException {
|
||||
if (filename == null) {
|
||||
throw new ParseException("The filename must not be null", 0);
|
||||
}
|
||||
|
||||
final String ext = ".json";
|
||||
if (!filename.endsWith(ext)) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
filename = filename.substring(0, filename.length() - ext.length());
|
||||
|
||||
final String delim = "-";
|
||||
String[] parts = filename.split(delim);
|
||||
if (parts.length < 3) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
|
||||
filename = TextUtils.join(delim, Arrays.copyOf(parts, parts.length - 2));
|
||||
if (!filename.equals(FILENAME_PREFIX)) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
|
||||
Date date = _dateFormat.parse(parts[parts.length - 2] + delim + parts[parts.length - 1]);
|
||||
if (date == null) {
|
||||
throwBadFormat(filename);
|
||||
}
|
||||
|
||||
return new FileInfo(filename, date);
|
||||
}
|
||||
|
||||
private static void throwBadFormat(String filename) throws ParseException {
|
||||
throw new ParseException(String.format("Bad backup filename format: %s", filename), 0);
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return _filename;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return _date;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s-%s.json", _filename, _dateFormat.format(_date));
|
||||
}
|
||||
}
|
||||
|
||||
private static class File {
|
||||
private DocumentFile _file;
|
||||
private FileInfo _info;
|
||||
|
||||
public File(DocumentFile file) throws ParseException {
|
||||
_file = file;
|
||||
_info = FileInfo.parseFilename(file.getName());
|
||||
}
|
||||
|
||||
public DocumentFile getFile() {
|
||||
return _file;
|
||||
}
|
||||
|
||||
public FileInfo getInfo() {
|
||||
return _info;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileComparator implements Comparator<File> {
|
||||
@Override
|
||||
public int compare(File o1, File o2) {
|
||||
return o1.getInfo().getDate().compareTo(o2.getInfo().getDate());
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/19503019
|
||||
private static class StrictDateFormat extends SimpleDateFormat {
|
||||
public StrictDateFormat(String pattern, Locale locale) {
|
||||
super(pattern, locale);
|
||||
setLenient(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date parse(@NonNull String text, ParsePosition pos) {
|
||||
int posIndex = pos.getIndex();
|
||||
Date d = super.parse(text, pos);
|
||||
if (!isLenient() && d != null) {
|
||||
String format = this.format(d);
|
||||
if (posIndex + format.length() != text.length() ||
|
||||
!text.endsWith(format)) {
|
||||
d = null; // Not exact match
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import android.content.Intent;
|
|||
|
||||
import androidx.core.util.AtomicFile;
|
||||
|
||||
import com.beemdevelopment.aegis.Preferences;
|
||||
import com.beemdevelopment.aegis.services.NotificationService;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
@ -18,9 +19,9 @@ import java.util.Collection;
|
|||
import java.util.TreeSet;
|
||||
|
||||
public class VaultManager {
|
||||
private static final String FILENAME = "aegis.json";
|
||||
public static final String FILENAME_EXPORT = "aegis_export.json";
|
||||
public static final String FILENAME_EXPORT_PLAIN = "aegis_export_plain.json";
|
||||
public static final String FILENAME = "aegis.json";
|
||||
public static final String FILENAME_EXPORT = "aegis-export.json";
|
||||
public static final String FILENAME_EXPORT_PLAIN = "aegis-export-plain.json";
|
||||
|
||||
private Vault _vault;
|
||||
private VaultFile _file;
|
||||
|
@ -28,9 +29,13 @@ public class VaultManager {
|
|||
private boolean _encrypt;
|
||||
|
||||
private Context _context;
|
||||
private Preferences _prefs;
|
||||
private VaultBackupManager _backups;
|
||||
|
||||
public VaultManager(Context context) {
|
||||
_context = context;
|
||||
_prefs = new Preferences(context);
|
||||
_backups = new VaultBackupManager(context);
|
||||
}
|
||||
|
||||
public boolean fileExists() {
|
||||
|
@ -102,6 +107,10 @@ public class VaultManager {
|
|||
_file.setContent(obj);
|
||||
}
|
||||
save(_context, _file);
|
||||
|
||||
if (_prefs.isBackupsEnabled()) {
|
||||
backup();
|
||||
}
|
||||
} catch (VaultFileException e) {
|
||||
throw new VaultManagerException(e);
|
||||
}
|
||||
|
@ -125,6 +134,11 @@ public class VaultManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void backup() throws VaultManagerException {
|
||||
assertState(false, true);
|
||||
_backups.create(_prefs.getBackupsLocation(), _prefs.getBackupsVersionCount());
|
||||
}
|
||||
|
||||
public void addEntry(VaultEntry entry) {
|
||||
assertState(false, true);
|
||||
_vault.getEntries().add(entry);
|
||||
|
|
|
@ -4,4 +4,8 @@ public class VaultManagerException extends Exception {
|
|||
public VaultManagerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VaultManagerException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<string name="pref_general_group_title">General</string>
|
||||
<string name="pref_security_group_title">Security</string>
|
||||
<string name="pref_tools_group_title">Tools</string>
|
||||
<string name="pref_backups_group_title">Backups</string>
|
||||
<string name="pref_select_theme_title">Theme</string>
|
||||
<string name="pref_view_mode_title">View mode</string>
|
||||
<string name="pref_lang_title">Language</string>
|
||||
|
@ -28,6 +29,14 @@
|
|||
<string name="pref_slots_summary">Manage the list of keys that can decrypt the vault</string>
|
||||
<string name="pref_import_file_title">Import from file</string>
|
||||
<string name="pref_import_file_summary">Import tokens from a file</string>
|
||||
<string name="pref_backups_title">Backup the vault</string>
|
||||
<string name="pref_backups_summary">Create backups of the vault on external storage. This is only supported for encrypted vaults.</string>
|
||||
<string name="pref_backups_location_title">Directory for backup files</string>
|
||||
<string name="pref_backups_location_summary">Backups will be stored at</string>
|
||||
<string name="pref_backups_trigger_title">Trigger backup</string>
|
||||
<string name="pref_backups_trigger_summary">Manually trigger a backup</string>
|
||||
<string name="pref_backups_versions_title">Amount of versions to keep</string>
|
||||
<string name="pref_backups_versions_summary">Keep %1$d versions of the backup</string>
|
||||
<string name="pref_import_app_title">Import from app</string>
|
||||
<string name="pref_import_app_summary">Import tokens from an app (requires root access)</string>
|
||||
<string name="pref_export_title">Export</string>
|
||||
|
|
|
@ -132,6 +132,32 @@
|
|||
app:iconSpaceReserved="false"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_backups_group_title"
|
||||
app:iconSpaceReserved="false">
|
||||
<androidx.preference.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_backups"
|
||||
android:title="@string/pref_backups_title"
|
||||
android:summary="@string/pref_backups_summary"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<Preference
|
||||
android:key="pref_backups_location"
|
||||
android:title="@string/pref_backups_location_title"
|
||||
android:summary="@string/pref_backups_location_summary"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<Preference
|
||||
android:key="pref_backups_trigger"
|
||||
android:title="@string/pref_backups_trigger_title"
|
||||
android:summary="@string/pref_backups_trigger_summary"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<Preference
|
||||
android:key="pref_backups_versions"
|
||||
android:title="@string/pref_backups_versions_title"
|
||||
android:summary="@string/pref_backups_versions_summary"
|
||||
app:iconSpaceReserved="false"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/pref_tools_group_title"
|
||||
app:iconSpaceReserved="false">
|
||||
|
|
Loading…
Add table
Reference in a new issue