Merge pull request #1439 from r3dh3ck/feature/single_backup

Single backup
This commit is contained in:
Alexander Bakker 2024-11-15 13:53:54 +01:00 committed by GitHub
commit b70654152d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 254 additions and 12 deletions

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum BackupsVersioningStrategy {
UNDEFINED,
MULTIPLE_BACKUPS,
SINGLE_BACKUP
}

View file

@ -6,7 +6,9 @@ import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.provider.DocumentsContractCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.beemdevelopment.aegis.util.JsonUtils; import com.beemdevelopment.aegis.util.JsonUtils;
@ -597,6 +599,19 @@ public class Preferences {
} }
} }
@NonNull
public BackupsVersioningStrategy getBackupVersioningStrategy() {
Uri uri = getBackupsLocation();
if (uri == null) {
return BackupsVersioningStrategy.UNDEFINED;
}
if (DocumentsContractCompat.isTreeUri(uri)) {
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
} else {
return BackupsVersioningStrategy.SINGLE_BACKUP;
}
}
public static class BackupResult { public static class BackupResult {
private final Date _time; private final Date _time;
private boolean _isBuiltIn; private boolean _isBuiltIn;

View file

@ -19,6 +19,8 @@ import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
import android.widget.NumberPicker; import android.widget.NumberPicker;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -27,6 +29,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper; import com.beemdevelopment.aegis.helpers.EditTextHelper;
@ -577,6 +580,54 @@ public class Dialogs {
showSecureDialog(dialog); showSecureDialog(dialog);
} }
public static void showBackupsVersioningStrategy(Context context, BackupsVersioningStrategy currentStrategy, BackupsVersioningStrategyListener listener) {
View view = LayoutInflater.from(context).inflate(R.layout.dialog_backups_versioning_strategy, null);
RadioGroup radioGroup = view.findViewById(R.id.radio_group);
RadioButton keepXVersionsButton = view.findViewById(R.id.keep_x_versions_button);
RadioButton singleBackupButton = view.findViewById(R.id.single_backup_button);
TextView warningText = view.findViewById(R.id.warning_text);
CheckBox riskAccept = view.findViewById(R.id.risk_accept);
final AtomicReference<Button> positiveButtonRef = new AtomicReference<>();
radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(checkedId == keepXVersionsButton.getId());
}
int visibility = checkedId == singleBackupButton.getId() ? View.VISIBLE : View.GONE;
warningText.setVisibility(visibility);
riskAccept.setVisibility(visibility);
});
riskAccept.setOnCheckedChangeListener((buttonView, isChecked) -> {
Button positiveButton = positiveButtonRef.get();
if (positiveButton != null) {
positiveButton.setEnabled(isChecked);
}
});
AlertDialog alertDialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.pref_backups_versioning_strategy_dialog_title)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int checkedId = radioGroup.getCheckedRadioButtonId();
if (checkedId == keepXVersionsButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.MULTIPLE_BACKUPS);
} else if (checkedId == singleBackupButton.getId()) {
listener.onStrategySelectionResult(BackupsVersioningStrategy.SINGLE_BACKUP);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
alertDialog.setOnShowListener(dialog -> {
Button positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButtonRef.set(positiveButton);
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
radioGroup.check(keepXVersionsButton.getId());
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
radioGroup.check(singleBackupButton.getId());
}
});
showSecureDialog(alertDialog);
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) { private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) { if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName())); view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
@ -605,4 +656,8 @@ public class Dialogs {
public interface ImporterListener { public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition); void onImporterSelectionResult(DatabaseImporter.Definition definition);
} }
public interface BackupsVersioningStrategyListener {
void onStrategySelectionResult(BackupsVersioningStrategy strategy);
}
} }

View file

@ -17,9 +17,11 @@ import androidx.annotation.Nullable;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat; import androidx.preference.SwitchPreferenceCompat;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.vault.VaultBackupManager;
import com.beemdevelopment.aegis.vault.VaultRepositoryException; import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import com.google.android.material.color.MaterialColors; import com.google.android.material.color.MaterialColors;
@ -27,6 +29,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private SwitchPreferenceCompat _androidBackupsPreference; private SwitchPreferenceCompat _androidBackupsPreference;
private SwitchPreferenceCompat _backupsPreference; private SwitchPreferenceCompat _backupsPreference;
private SwitchPreferenceCompat _backupReminderPreference; private SwitchPreferenceCompat _backupReminderPreference;
private Preference _versioningStrategyPreference;
private Preference _backupsLocationPreference; private Preference _backupsLocationPreference;
private Preference _backupsTriggerPreference; private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference; private Preference _backupsVersionsPreference;
@ -75,7 +78,13 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference = requirePreference("pref_backups"); _backupsPreference = requirePreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { _backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) { if ((boolean) newValue) {
selectBackupsLocation(); Dialogs.showBackupsVersioningStrategy(requireContext(), BackupsVersioningStrategy.MULTIPLE_BACKUPS, strategy -> {
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
} else { } else {
_prefs.setIsBackupsEnabled(false); _prefs.setIsBackupsEnabled(false);
updateBackupPreference(); updateBackupPreference();
@ -100,6 +109,24 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false; return false;
}); });
_versioningStrategyPreference = requirePreference("pref_versioning_strategy");
updateBackupsVersioningStrategySummary();
_versioningStrategyPreference.setOnPreferenceClickListener(preference -> {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
Dialogs.showBackupsVersioningStrategy(requireContext(), currentStrategy, strategy -> {
if (strategy == currentStrategy) {
return;
}
if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
});
return true;
});
_androidBackupsPreference = requirePreference("pref_android_backups"); _androidBackupsPreference = requirePreference("pref_android_backups");
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { _androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue); _prefs.setIsAndroidBackupsEnabled((boolean) newValue);
@ -110,13 +137,15 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return false; return false;
}); });
Uri backupLocation = _prefs.getBackupsLocation();
_backupsLocationPreference = requirePreference("pref_backups_location"); _backupsLocationPreference = requirePreference("pref_backups_location");
if (backupLocation != null) { updateBackupsLocationSummary();
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(backupLocation.toString())));
}
_backupsLocationPreference.setOnPreferenceClickListener(preference -> { _backupsLocationPreference.setOnPreferenceClickListener(preference -> {
selectBackupsLocation(); BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
selectBackupsLocation();
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackupFile();
}
return false; return false;
}); });
@ -158,9 +187,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_prefs.setBackupsLocation(uri); _prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true); _prefs.setIsBackupsEnabled(true);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference(); updateBackupPreference();
scheduleBackup(); scheduleBackup();
updateBackupsVersioningStrategySummary();
updateBackupsLocationSummary();
} }
private void updateBackupPreference() { private void updateBackupPreference() {
@ -174,9 +204,10 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsPreference.setChecked(backupEnabled); _backupsPreference.setChecked(backupEnabled);
_backupsPreference.setEnabled(encrypted); _backupsPreference.setEnabled(encrypted);
_backupReminderPreference.setChecked(backupReminderEnabled); _backupReminderPreference.setChecked(backupReminderEnabled);
_versioningStrategyPreference.setVisible(backupEnabled);
_backupsLocationPreference.setVisible(backupEnabled); _backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled); _backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled); _backupsVersionsPreference.setVisible(backupEnabled && _prefs.getBackupVersioningStrategy() != BackupsVersioningStrategy.SINGLE_BACKUP);
if (backupEnabled) { if (backupEnabled) {
updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult()); updateBackupStatus(_builtinBackupStatusPreference, _prefs.getBuiltInBackupResult());
} }
@ -221,6 +252,14 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
return spannable; return spannable;
} }
private void createBackupFile() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/json")
.putExtra(Intent.EXTRA_TITLE, VaultBackupManager.FILENAME_SINGLE);
_vaultManager.fireIntentLauncher(this, intent, backupsResultLauncher);
}
private void selectBackupsLocation() { private void selectBackupsLocation() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
@ -241,6 +280,30 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
} }
} }
private void updateBackupsVersioningStrategySummary() {
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_keep_x_versions);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
_versioningStrategyPreference.setSummary(R.string.pref_backups_versioning_strategy_single_backup);
}
}
private void updateBackupsLocationSummary() {
Uri backupsLocation = _prefs.getBackupsLocation();
BackupsVersioningStrategy currentStrategy = _prefs.getBackupVersioningStrategy();
String text;
if (currentStrategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
text = getString(R.string.pref_backups_location_summary);
} else if (currentStrategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
text = getString(R.string.pref_backup_location_summary);
} else {
return;
}
String summary = String.format("%s: %s", text, Uri.decode(backupsLocation.toString()));
_backupsLocationPreference.setSummary(summary);
}
private void updateBackupsVersionsSummary() { private void updateBackupsVersionsSummary() {
int count = _prefs.getBackupsVersionCount(); int count = _prefs.getBackupsVersionCount();
if (count == Preferences.BACKUPS_VERSIONS_INFINITE) { if (count == Preferences.BACKUPS_VERSIONS_INFINITE) {

View file

@ -1,5 +1,6 @@
package com.beemdevelopment.aegis.vault; package com.beemdevelopment.aegis.vault;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.UriPermission; import android.content.UriPermission;
import android.net.Uri; import android.net.Uri;
@ -9,6 +10,7 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile; import androidx.documentfile.provider.DocumentFile;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.database.AuditLogRepository; import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.util.IOUtils; import com.beemdevelopment.aegis.util.IOUtils;
@ -38,6 +40,7 @@ public class VaultBackupManager {
new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH); new StrictDateFormat("yyyyMMdd-HHmmss", Locale.ENGLISH);
public static final String FILENAME_PREFIX = "aegis-backup"; public static final String FILENAME_PREFIX = "aegis-backup";
public static final String FILENAME_SINGLE = String.format("%s.json", FILENAME_PREFIX);
private final Context _context; private final Context _context;
private final Preferences _prefs; private final Preferences _prefs;
@ -51,10 +54,10 @@ public class VaultBackupManager {
_auditLogRepository = auditLogRepository; _auditLogRepository = auditLogRepository;
} }
public void scheduleBackup(File tempFile, Uri dirUri, int versionsToKeep) { public void scheduleBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep) {
_executor.execute(() -> { _executor.execute(() -> {
try { try {
createBackup(tempFile, dirUri, versionsToKeep); createBackup(tempFile, strategy, uri, versionsToKeep);
_auditLogRepository.addBackupCreatedEvent(); _auditLogRepository.addBackupCreatedEvent();
_prefs.setBuiltInBackupResult(new Preferences.BackupResult(null)); _prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
} catch (VaultRepositoryException | VaultBackupPermissionException e) { } catch (VaultRepositoryException | VaultBackupPermissionException e) {
@ -64,6 +67,46 @@ public class VaultBackupManager {
}); });
} }
private void createBackup(File tempFile, BackupsVersioningStrategy strategy, Uri uri, int versionsToKeep)
throws VaultRepositoryException, VaultBackupPermissionException {
if (uri == null) {
throw new VaultRepositoryException("getBackupsLocation returned null");
}
if (strategy == BackupsVersioningStrategy.SINGLE_BACKUP) {
createBackup(tempFile, uri);
} else if (strategy == BackupsVersioningStrategy.MULTIPLE_BACKUPS) {
createBackup(tempFile, uri, versionsToKeep);
} else {
throw new VaultRepositoryException("Invalid backups versioning strategy");
}
}
private void createBackup(File tempFile, Uri fileUri)
throws VaultRepositoryException, VaultBackupPermissionException {
Log.i(TAG, String.format("Creating backup at %s", fileUri));
try {
if (!hasPermissionsAt(fileUri)) {
throw new VaultBackupPermissionException("No persisted URI permissions");
}
ContentResolver resolver = _context.getContentResolver();
try (FileInputStream inStream = new FileInputStream(tempFile);
OutputStream outStream = resolver.openOutputStream(fileUri, "wt")
) {
if (outStream == null) {
throw new IOException("openOutputStream returned null");
}
IOUtils.copy(inStream, outStream);
} catch (IOException exception) {
throw new VaultRepositoryException(exception);
}
} catch (VaultRepositoryException | VaultBackupPermissionException exception) {
Log.e(TAG, String.format("Unable to create backup: %s", exception));
throw exception;
} finally {
tempFile.delete();
}
}
private void createBackup(File tempFile, Uri dirUri, int versionsToKeep) private void createBackup(File tempFile, Uri dirUri, int versionsToKeep)
throws VaultRepositoryException, VaultBackupPermissionException { throws VaultRepositoryException, VaultBackupPermissionException {
FileInfo fileInfo = new FileInfo(FILENAME_PREFIX); FileInfo fileInfo = new FileInfo(FILENAME_PREFIX);

View file

@ -5,12 +5,14 @@ import android.app.backup.BackupManager;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.beemdevelopment.aegis.BackupsVersioningStrategy;
import com.beemdevelopment.aegis.Preferences; import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle; import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
@ -172,8 +174,11 @@ public class VaultManager {
try (OutputStream outStream = new FileOutputStream(tempFile)) { try (OutputStream outStream = new FileOutputStream(tempFile)) {
_repo.export(outStream); _repo.export(outStream);
} }
BackupsVersioningStrategy strategy = _prefs.getBackupVersioningStrategy();
Uri uri = _prefs.getBackupsLocation();
int versionsToKeep = _prefs.getBackupsVersionCount();
_backups.scheduleBackup(tempFile, _prefs.getBackupsLocation(), _prefs.getBackupsVersionCount()); _backups.scheduleBackup(tempFile, strategy, uri, versionsToKeep);
} catch (IOException e) { } catch (IOException e) {
throw new VaultRepositoryException(e); throw new VaultRepositoryException(e);
} }

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="20dp"
android:paddingTop="20dp"
android:paddingEnd="20dp"
android:orientation="vertical">
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/keep_x_versions_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_backups_versioning_strategy_keep_x_versions" />
<RadioButton
android:id="@+id/single_backup_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_backups_versioning_strategy_single_backup" />
</RadioGroup>
<TextView
android:id="@+id/warning_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:text="@string/pref_backups_versioning_strategy_single_backup_warning"
android:textColor="?attr/colorError" />
<CheckBox
android:id="@+id/risk_accept"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/understand_risk_accept" />
</LinearLayout>

View file

@ -67,8 +67,14 @@
<string name="pref_backups_reminder_summary">Show a reminder to backup your vault in case you haven\'t backed up your latest changes.</string> <string name="pref_backups_reminder_summary">Show a reminder to backup your vault in case you haven\'t backed up your latest changes.</string>
<string name="pref_backups_reminder_dialog_title">Disable backup reminder</string> <string name="pref_backups_reminder_dialog_title">Disable backup reminder</string>
<string name="pref_backups_reminder_dialog_summary">Disabling this reminder means Aegis won\'t tell you whether or not you have changes that are not backed up yet. This puts you at risk of losing access to your tokens. Are you sure you want to disable the reminder?</string> <string name="pref_backups_reminder_dialog_summary">Disabling this reminder means Aegis won\'t tell you whether or not you have changes that are not backed up yet. This puts you at risk of losing access to your tokens. Are you sure you want to disable the reminder?</string>
<string name="pref_backups_location_title">Directory for backup files</string> <string name="pref_backups_versioning_strategy_title">Backup strategy</string>
<string name="pref_backups_versioning_strategy_keep_x_versions">Keep a number of versions</string>
<string name="pref_backups_versioning_strategy_single_backup">Single backup</string>
<string name="pref_backups_versioning_strategy_single_backup_warning">The selected backup strategy is not reliable and not recommended. A single backup failure could lead to losing your only backup.</string>
<string name="pref_backups_versioning_strategy_dialog_title">Select a backup strategy</string>
<string name="pref_backups_location_title">Backup location</string>
<string name="pref_backups_location_summary">Backups will be stored at</string> <string name="pref_backups_location_summary">Backups will be stored at</string>
<string name="pref_backup_location_summary">Backup will be stored at</string>
<string name="pref_backups_trigger_title">Trigger backup</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_trigger_summary">Manually trigger a backup</string>
<string name="pref_backups_versions_title">Number of versions to keep</string> <string name="pref_backups_versions_title">Number of versions to keep</string>

View file

@ -23,6 +23,10 @@
android:title="@string/pref_backups_reminder_title" android:title="@string/pref_backups_reminder_title"
android:summary="@string/pref_backups_reminder_summary" /> android:summary="@string/pref_backups_reminder_summary" />
<Preference
android:key="pref_versioning_strategy"
android:title="@string/pref_backups_versioning_strategy_title" />
<Preference <Preference
android:key="pref_backups_location" android:key="pref_backups_location"
android:title="@string/pref_backups_location_title" android:title="@string/pref_backups_location_title"