Store and display backup error messages more clearly

This commit is contained in:
Alexander Bakker 2022-10-05 18:21:50 +02:00
parent 4427498d5e
commit 8ae8130b71
10 changed files with 242 additions and 34 deletions

View file

@ -29,7 +29,7 @@ public class AegisBackupAgent extends BackupAgent {
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
// cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore // Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
_prefs = new Preferences(this); _prefs = new Preferences(this);
} }
@ -40,7 +40,7 @@ public class AegisBackupAgent extends BackupAgent {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1)); Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER; && (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
if (isD2D) { if (isD2D) {
Log.i(TAG, "onFullBackup(): allowing D2D transfer"); Log.i(TAG, "onFullBackup(): allowing D2D transfer");
@ -49,31 +49,39 @@ public class AegisBackupAgent extends BackupAgent {
return; return;
} }
// first copy the vault to the files/backup directory // We perform a catch of any Exception here to make sure we also
// report any runtime exceptions, in addition to the expected IOExceptions.
try {
fullBackup(data);
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
} catch (Exception e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
_prefs.setAndroidBackupResult(new Preferences.BackupResult(e));
throw e;
}
Log.i(TAG, "onFullBackup() finished");
}
private void fullBackup(FullBackupDataOutput data) throws IOException {
// First copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile(); File vaultBackupFile = getVaultBackupFile();
try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) { try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) {
createBackupDir();
VaultFile vaultFile = VaultRepository.readVaultFile(this); VaultFile vaultFile = VaultRepository.readVaultFile(this);
byte[] bytes = vaultFile.exportable().toBytes(); byte[] bytes = vaultFile.exportable().toBytes();
outputStream.write(bytes); outputStream.write(bytes);
} catch (VaultRepositoryException | IOException e) { } catch (VaultRepositoryException | IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
deleteBackupDir(); deleteBackupDir();
throw new IOException(e); throw new IOException(e);
} }
// then call the original implementation so that fullBackupContent specified in AndroidManifest is read // Then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try { try {
super.onFullBackup(data); super.onFullBackup(data);
} catch (IOException e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
throw e;
} finally { } finally {
deleteBackupDir(); deleteBackupDir();
} }
Log.i(TAG, "onFullBackup() finished");
} }
@Override @Override
@ -114,14 +122,16 @@ public class AegisBackupAgent extends BackupAgent {
private void createBackupDir() throws IOException { private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile(); File dir = getVaultBackupFile().getParentFile();
if (!dir.exists() && !dir.mkdir()) { if (dir == null || (!dir.exists() && !dir.mkdir())) {
throw new IOException(String.format("Unable to create backup directory: %s", dir.toString())); throw new IOException(String.format("Unable to create backup directory: %s", dir));
} }
} }
private void deleteBackupDir() { private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile(); File dir = getVaultBackupFile().getParentFile();
IOUtils.clearDirectory(dir, true); if (dir != null) {
IOUtils.clearDirectory(dir, true);
}
} }
private File getVaultBackupFile() { private File getVaultBackupFile() {

View file

@ -7,10 +7,15 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.text.DateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
@ -259,6 +264,7 @@ public class Preferences {
public void setIsAndroidBackupsEnabled(boolean enabled) { public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply(); _prefs.edit().putBoolean("pref_android_backups", enabled).apply();
setAndroidBackupResult(null);
} }
public boolean isBackupsEnabled() { public boolean isBackupsEnabled() {
@ -267,6 +273,7 @@ public class Preferences {
public void setIsBackupsEnabled(boolean enabled) { public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply(); _prefs.edit().putBoolean("pref_backups", enabled).apply();
setBuiltInBackupResult(null);
} }
public Uri getBackupsLocation() { public Uri getBackupsLocation() {
@ -298,12 +305,64 @@ public class Preferences {
_prefs.edit().putInt("pref_backups_versions", versions).apply(); _prefs.edit().putInt("pref_backups_versions", versions).apply();
} }
public void setBackupsError(Exception e) { public void setAndroidBackupResult(@Nullable BackupResult res) {
_prefs.edit().putString("pref_backups_error", e == null ? null : e.toString()).apply(); setBackupResult(false, res);
} }
public String getBackupsError() { public void setBuiltInBackupResult(@Nullable BackupResult res) {
return _prefs.getString("pref_backups_error", null); setBackupResult(true, res);
}
@Nullable
public BackupResult getAndroidBackupResult() {
return getBackupResult(false);
}
@Nullable
public BackupResult getBuiltInBackupResult() {
return getBackupResult(true);
}
@Nullable
public Preferences.BackupResult getErroredBackupResult() {
Preferences.BackupResult res = getBuiltInBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
res = getAndroidBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
return null;
}
private void setBackupResult(boolean isBuiltInBackup, @Nullable BackupResult res) {
String json = null;
if (res != null) {
res.setIsBuiltIn(isBuiltInBackup);
json = res.toJson();
}
_prefs.edit().putString(getBackupResultKey(isBuiltInBackup), json).apply();
}
@Nullable
private BackupResult getBackupResult(boolean isBuiltInBackup) {
String json = _prefs.getString(getBackupResultKey(isBuiltInBackup), null);
if (json == null) {
return null;
}
try {
BackupResult res = BackupResult.fromJson(json);
res.setIsBuiltIn(isBuiltInBackup);
return res;
} catch (JSONException e) {
return null;
}
}
private static String getBackupResultKey(boolean isBuiltInBackup) {
return isBuiltInBackup ? "pref_backups_result_builtin": "pref_backups_result_android";
} }
public void setIsBackupReminderNeeded(boolean needed) { public void setIsBackupReminderNeeded(boolean needed) {
@ -382,4 +441,63 @@ public class Preferences {
} }
} }
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString());
}
private BackupResult(Date time, @Nullable String error) {
_time = time;
_error = error;
}
@Nullable
public String getError() {
return _error;
}
public boolean isSuccessful() {
return _error == null;
}
public Date getTime() {
return _time;
}
public String getHumanReadableTime() {
return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(_time);
}
public boolean isBuiltIn() {
return _isBuiltIn;
}
private void setIsBuiltIn(boolean isBuiltIn) {
_isBuiltIn = isBuiltIn;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj.toString();
}
public static BackupResult fromJson(String json) throws JSONException {
JSONObject obj = new JSONObject(json);
long time = obj.getLong("time");
String error = JsonUtils.optString(obj, "error");
return new BackupResult(new Date(time), error);
}
}
} }

View file

@ -41,7 +41,6 @@ import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper; import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException; import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs; import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment; import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
@ -781,15 +780,13 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
} }
private void updateErrorBar() { private void updateErrorBar() {
String backupError = null; Preferences.BackupResult backupRes = _prefs.getErroredBackupResult();
if (_prefs.isBackupsEnabled()) { if (backupRes != null) {
backupError = _prefs.getBackupsError();
}
if (backupError != null) {
_textErrorBar.setText(R.string.backup_error_bar_message); _textErrorBar.setText(R.string.backup_error_bar_message);
_btnErrorBar.setOnClickListener(view -> { _btnErrorBar.setOnClickListener(view -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups"); Dialogs.showBackupErrorDialog(this, backupRes, (dialog, which) -> {
startPreferencesActivity(BackupsPreferencesFragment.class, "pref_backups");
});
}); });
_btnErrorBar.setVisibility(View.VISIBLE); _btnErrorBar.setVisibility(View.VISIBLE);
} else if (_prefs.isBackupsReminderNeeded()) { } else if (_prefs.isBackupsReminderNeeded()) {

View file

@ -36,6 +36,7 @@ import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper; import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper; import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter; import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.fragments.preferences.BackupsPreferencesFragment;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask; import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot; import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
@ -403,6 +404,12 @@ public class Dialogs {
Dialogs.showSecureDialog(dialog); Dialogs.showSecureDialog(dialog);
} }
public static void showBackupErrorDialog(Context context, Preferences.BackupResult backupRes, DialogInterface.OnClickListener listener) {
String system = context.getString(backupRes.isBuiltIn() ? R.string.backup_system_builtin : R.string.backup_system_android);
String message = context.getString(R.string.backup_error_dialog_details, system, backupRes.getHumanReadableTime());
Dialogs.showErrorDialog(context, message, backupRes.getError(), listener);
}
public static void showMultiMessageDialog( public static void showMultiMessageDialog(
Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) { Context context, @StringRes int title, String message, List<CharSequence> messages, DialogInterface.OnClickListener listener) {
Dialogs.showSecureDialog(new AlertDialog.Builder(context) Dialogs.showSecureDialog(new AlertDialog.Builder(context)

View file

@ -2,10 +2,16 @@ package com.beemdevelopment.aegis.ui.fragments.preferences;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat; import androidx.preference.SwitchPreferenceCompat;
@ -21,6 +27,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
private Preference _backupsTriggerPreference; private Preference _backupsTriggerPreference;
private Preference _backupsVersionsPreference; private Preference _backupsVersionsPreference;
private Preference _builtinBackupStatusPreference;
private Preference _androidBackupStatusPreference;
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
@ -32,6 +41,23 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
super.onCreatePreferences(savedInstanceState, rootKey); super.onCreatePreferences(savedInstanceState, rootKey);
addPreferencesFromResource(R.xml.preferences_backups); addPreferencesFromResource(R.xml.preferences_backups);
_builtinBackupStatusPreference = requirePreference("pref_status_backup_builtin");
_builtinBackupStatusPreference.setOnPreferenceClickListener(preference -> {
Preferences.BackupResult backupRes = _prefs.getBuiltInBackupResult();
if (backupRes != null && !backupRes.isSuccessful()) {
Dialogs.showBackupErrorDialog(requireContext(), backupRes, null);
}
return true;
});
_androidBackupStatusPreference = requirePreference("pref_status_backup_android");
_androidBackupStatusPreference.setOnPreferenceClickListener(preference -> {
Preferences.BackupResult backupRes = _prefs.getAndroidBackupResult();
if (backupRes != null && !backupRes.isSuccessful()) {
Dialogs.showBackupErrorDialog(requireContext(), backupRes, null);
}
return true;
});
_backupsPreference = requirePreference("pref_backups"); _backupsPreference = requirePreference("pref_backups");
_backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { _backupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if ((boolean) newValue) { if ((boolean) newValue) {
@ -48,7 +74,9 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> { _androidBackupsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
_prefs.setIsAndroidBackupsEnabled((boolean) newValue); _prefs.setIsAndroidBackupsEnabled((boolean) newValue);
updateBackupPreference(); updateBackupPreference();
_vaultManager.scheduleAndroidBackup(); if ((boolean) newValue) {
_vaultManager.scheduleAndroidBackup();
}
return false; return false;
}); });
@ -66,6 +94,7 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsTriggerPreference.setOnPreferenceClickListener(preference -> { _backupsTriggerPreference.setOnPreferenceClickListener(preference -> {
if (_prefs.isBackupsEnabled()) { if (_prefs.isBackupsEnabled()) {
scheduleBackup(); scheduleBackup();
_builtinBackupStatusPreference.setVisible(false);
} }
return true; return true;
}); });
@ -100,7 +129,6 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_prefs.setBackupsLocation(uri); _prefs.setBackupsLocation(uri);
_prefs.setIsBackupsEnabled(true); _prefs.setIsBackupsEnabled(true);
_prefs.setBackupsError(null);
_backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString()))); _backupsLocationPreference.setSummary(String.format("%s: %s", getString(R.string.pref_backups_location_summary), Uri.decode(uri.toString())));
updateBackupPreference(); updateBackupPreference();
scheduleBackup(); scheduleBackup();
@ -117,6 +145,38 @@ public class BackupsPreferencesFragment extends PreferencesFragment {
_backupsLocationPreference.setVisible(backupEnabled); _backupsLocationPreference.setVisible(backupEnabled);
_backupsTriggerPreference.setVisible(backupEnabled); _backupsTriggerPreference.setVisible(backupEnabled);
_backupsVersionsPreference.setVisible(backupEnabled); _backupsVersionsPreference.setVisible(backupEnabled);
if (backupEnabled) {
Preferences.BackupResult backupRes = _prefs.getBuiltInBackupResult();
_builtinBackupStatusPreference.setSummary(getBackupStatusMessage(backupRes));
_builtinBackupStatusPreference.setSelectable(backupRes != null && !backupRes.isSuccessful());
}
if (androidBackupEnabled) {
Preferences.BackupResult backupRes = _prefs.getAndroidBackupResult();
_androidBackupStatusPreference.setSummary(getBackupStatusMessage(backupRes));
_androidBackupStatusPreference.setSelectable(backupRes != null && !backupRes.isSuccessful());
}
_builtinBackupStatusPreference.setVisible(backupEnabled);
_androidBackupStatusPreference.setVisible(androidBackupEnabled);
}
private CharSequence getBackupStatusMessage(@Nullable Preferences.BackupResult res) {
String message;
int color = R.color.warning_color;
if (res == null) {
message = getString(R.string.backup_status_none);
} else if (res.isSuccessful()) {
color = R.color.success_color;
message = getString(R.string.backup_status_success, res.getHumanReadableTime());
} else {
message = getString(R.string.backup_status_failed, res.getHumanReadableTime());
}
Spannable spannable = new SpannableString(message);
spannable.setSpan(new ForegroundColorSpan(getResources().getColor(color)), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (color == R.color.warning_color) {
spannable.setSpan(new StyleSpan(Typeface.BOLD), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return spannable;
} }
private void selectBackupsLocation() { private void selectBackupsLocation() {

View file

@ -52,10 +52,10 @@ public class VaultBackupManager {
_executor.execute(() -> { _executor.execute(() -> {
try { try {
createBackup(tempFile, dirUri, versionsToKeep); createBackup(tempFile, dirUri, versionsToKeep);
_prefs.setBackupsError(null); _prefs.setBuiltInBackupResult(new Preferences.BackupResult(null));
} catch (VaultRepositoryException e) { } catch (VaultRepositoryException e) {
e.printStackTrace(); e.printStackTrace();
_prefs.setBackupsError(e); _prefs.setBuiltInBackupResult(new Preferences.BackupResult(e));
} }
}); });
} }

View file

@ -126,7 +126,7 @@ public class VaultFile {
return this; return this;
} }
return new VaultFile(getContent(), new VaultFile.Header( return new VaultFile(_content, new VaultFile.Header(
getHeader().getSlots().exportable(), getHeader().getSlots().exportable(),
getHeader().getParams() getHeader().getParams()
)); ));

View file

@ -196,9 +196,8 @@ public class VaultManager {
backedUp = true; backedUp = true;
try { try {
scheduleBackup(); scheduleBackup();
_prefs.setBackupsError(null);
} catch (VaultRepositoryException e) { } catch (VaultRepositoryException e) {
_prefs.setBackupsError(e); _prefs.setBuiltInBackupResult(new Preferences.BackupResult(e));
} }
} }

View file

@ -205,6 +205,9 @@
<string name="disable_encryption_error">An error occurred while disabling encryption</string> <string name="disable_encryption_error">An error occurred while disabling encryption</string>
<string name="backup_successful">The backup was scheduled successfully</string> <string name="backup_successful">The backup was scheduled successfully</string>
<string name="backup_error">An error occurred while trying to create a backup</string> <string name="backup_error">An error occurred while trying to create a backup</string>
<string name="backup_status_success">Most recent backup successful: %s</string>
<string name="backup_status_failed">Most recent backup failed: %s</string>
<string name="backup_status_none">No backups have been made yet</string>
<string name="documentsui_error">DocumentsUI appears to be missing from your device. This is an important system component necessary for the selection and creation of documents. If you used a tool to &quot;debloat&quot; your device, you may have accidentally deleted it and will have to reinstall it.</string> <string name="documentsui_error">DocumentsUI appears to be missing from your device. This is an important system component necessary for the selection and creation of documents. If you used a tool to &quot;debloat&quot; your device, you may have accidentally deleted it and will have to reinstall it.</string>
<string name="icon_pack_import_error">An error occurred while trying to import an icon pack</string> <string name="icon_pack_import_error">An error occurred while trying to import an icon pack</string>
<string name="icon_pack_import_exists_error">The icon pack you\'re trying to import already exists. Do you want to overwrite it?</string> <string name="icon_pack_import_exists_error">The icon pack you\'re trying to import already exists. Do you want to overwrite it?</string>
@ -351,6 +354,9 @@
</plurals> </plurals>
<string name="google_qr_export_unexpected">Expected QR code #%d, but scanned #%d instead</string> <string name="google_qr_export_unexpected">Expected QR code #%d, but scanned #%d instead</string>
<string name="backup_error_bar_message"><b>Vault backup failed recently</b></string> <string name="backup_error_bar_message"><b>Vault backup failed recently</b></string>
<string name="backup_error_dialog_details">A recent vault backup attempt using %s failed because an error occurred. The backup was attempted at: %s. Please check your backup settings to make sure backups can complete successfully.</string>
<string name="backup_system_builtin">Aegis\' built-in automatic backups</string>
<string name="backup_system_android">Android\'s cloud backup system</string>
<string name="backup_reminder_bar_message"><b>Recent vault changes are not backed up</b></string> <string name="backup_reminder_bar_message"><b>Recent vault changes are not backed up</b></string>
<string name="backup_plaintext_export_warning"><b>The vault was recently exported in plain text</b></string> <string name="backup_plaintext_export_warning"><b>The vault was recently exported in plain text</b></string>
<string name="pref_show_plaintext_warning_hint">Don\'t show this warning again</string> <string name="pref_show_plaintext_warning_hint">Don\'t show this warning again</string>

View file

@ -25,6 +25,11 @@
android:key="pref_backups_versions" android:key="pref_backups_versions"
android:title="@string/pref_backups_versions_title" android:title="@string/pref_backups_versions_title"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
<Preference
android:key="pref_status_backup_builtin"
android:persistent="false"
android:selectable="false"
app:iconSpaceReserved="false"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_cat_backups_android" android:title="@string/pref_cat_backups_android"
@ -36,6 +41,12 @@
android:summary="@string/pref_android_backups_summary" android:summary="@string/pref_android_backups_summary"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>
<Preference <Preference
android:key="pref_status_backup_android"
android:persistent="false"
android:selectable="false"
app:iconSpaceReserved="false"/>
<Preference
android:persistent="false"
android:selectable="false" android:selectable="false"
android:summary="@string/pref_android_backups_hint" android:summary="@string/pref_android_backups_hint"
app:iconSpaceReserved="false"/> app:iconSpaceReserved="false"/>