BackupSettingsFragment.java

  1package eu.siacs.conversations.ui.fragment.settings;
  2
  3import android.Manifest;
  4import android.content.ActivityNotFoundException;
  5import android.content.Intent;
  6import android.content.pm.PackageManager;
  7import android.net.Uri;
  8import android.os.Build;
  9import android.os.Bundle;
 10import android.util.Log;
 11import android.widget.Toast;
 12import androidx.activity.result.ActivityResultLauncher;
 13import androidx.activity.result.contract.ActivityResultContracts;
 14import androidx.annotation.NonNull;
 15import androidx.annotation.Nullable;
 16import androidx.appcompat.app.AlertDialog;
 17import androidx.core.content.ContextCompat;
 18import androidx.preference.ListPreference;
 19import androidx.preference.Preference;
 20import androidx.work.Constraints;
 21import androidx.work.Data;
 22import androidx.work.ExistingPeriodicWorkPolicy;
 23import androidx.work.ExistingWorkPolicy;
 24import androidx.work.OneTimeWorkRequest;
 25import androidx.work.OutOfQuotaPolicy;
 26import androidx.work.PeriodicWorkRequest;
 27import androidx.work.WorkManager;
 28import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 29import com.google.common.base.Strings;
 30import com.google.common.primitives.Longs;
 31import eu.siacs.conversations.AppSettings;
 32import eu.siacs.conversations.Config;
 33import eu.siacs.conversations.R;
 34import eu.siacs.conversations.entities.Account;
 35import eu.siacs.conversations.persistance.FileBackend;
 36import eu.siacs.conversations.worker.ExportBackupWorker;
 37import java.util.concurrent.TimeUnit;
 38
 39public class BackupSettingsFragment extends XmppPreferenceFragment {
 40
 41    public static final String CREATE_ONE_OFF_BACKUP = "create_one_off_backup";
 42    private static final String RECURRING_BACKUP = "recurring_backup";
 43
 44    private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
 45            registerForActivityResult(
 46                    new ActivityResultContracts.RequestPermission(),
 47                    isGranted -> {
 48                        if (isGranted) {
 49                            startOneOffBackup();
 50                        } else {
 51                            Toast.makeText(
 52                                            requireActivity(),
 53                                            getString(
 54                                                    R.string.no_storage_permission,
 55                                                    getString(R.string.app_name)),
 56                                            Toast.LENGTH_LONG)
 57                                    .show();
 58                        }
 59                    });
 60
 61    private final ActivityResultLauncher<Uri> pickBackupLocationLauncher =
 62            registerForActivityResult(
 63                    new ActivityResultContracts.OpenDocumentTree(),
 64                    uri -> {
 65                        if (uri == null) {
 66                            Log.d(Config.LOGTAG, "no backup location selected");
 67                            return;
 68                        }
 69                        submitBackupLocationPreference(uri);
 70                    });
 71
 72    @Override
 73    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
 74        setPreferencesFromResource(R.xml.preferences_backup, rootKey);
 75        final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP);
 76        final var export = findPreference("export");
 77        final ListPreference recurringBackup = findPreference(RECURRING_BACKUP);
 78        final var backupLocation = findPreference(AppSettings.BACKUP_LOCATION);
 79        if (createOneOffBackup == null || recurringBackup == null || backupLocation == null) {
 80            throw new IllegalStateException(
 81                    "The preference resource file is missing some preferences");
 82        }
 83        final var appSettings = new AppSettings(requireContext());
 84        backupLocation.setSummary(
 85                getString(
 86                        R.string.pref_create_backup_summary,
 87                        appSettings.getBackupLocationAsPath()));
 88        backupLocation.setOnPreferenceClickListener(this::onBackupLocationPreferenceClicked);
 89        createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
 90        export.setOnPreferenceClickListener(this::onExportClicked);
 91        setValues(
 92                recurringBackup,
 93                R.array.recurring_backup_values,
 94                value -> timeframeValueToName(requireContext(), value));
 95    }
 96
 97    private boolean onBackupLocationPreferenceClicked(final Preference preference) {
 98        try {
 99            this.pickBackupLocationLauncher.launch(null);
100        } catch (final ActivityNotFoundException e) {
101            Toast.makeText(requireActivity(), R.string.no_application_found, Toast.LENGTH_LONG)
102                    .show();
103        }
104        return false;
105    }
106
107    private void submitBackupLocationPreference(final Uri uri) {
108        final var contentResolver = requireContext().getContentResolver();
109        contentResolver.takePersistableUriPermission(
110                uri,
111                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
112        final var appSettings = new AppSettings(requireContext());
113        appSettings.setBackupLocation(uri);
114        final var preference = findPreference(AppSettings.BACKUP_LOCATION);
115        if (preference == null) {
116            return;
117        }
118        preference.setSummary(
119                getString(R.string.pref_create_backup_summary, AppSettings.asPath(uri)));
120    }
121
122    @Override
123    protected void onSharedPreferenceChanged(@NonNull String key) {
124        super.onSharedPreferenceChanged(key);
125        if (RECURRING_BACKUP.equals(key)) {
126            final var sharedPreferences = getPreferenceManager().getSharedPreferences();
127            if (sharedPreferences == null) {
128                return;
129            }
130            final Long recurringBackupInterval =
131                    Longs.tryParse(
132                            Strings.nullToEmpty(
133                                    sharedPreferences.getString(RECURRING_BACKUP, null)));
134            if (recurringBackupInterval == null) {
135                return;
136            }
137            Log.d(
138                    Config.LOGTAG,
139                    "recurring backup interval changed to: " + recurringBackupInterval);
140            final var workManager = WorkManager.getInstance(requireContext());
141            if (recurringBackupInterval <= 0) {
142                workManager.cancelUniqueWork(RECURRING_BACKUP);
143            } else {
144                final Constraints constraints =
145                        new Constraints.Builder()
146                                .setRequiresBatteryNotLow(true)
147                                .setRequiresStorageNotLow(true)
148                                .build();
149
150                final PeriodicWorkRequest periodicWorkRequest =
151                        new PeriodicWorkRequest.Builder(
152                                        ExportBackupWorker.class,
153                                        recurringBackupInterval,
154                                        TimeUnit.SECONDS)
155                                .setConstraints(constraints)
156                                .setInputData(
157                                        new Data.Builder()
158                                                .putBoolean("recurring_backup", true)
159                                                .build())
160                                .build();
161                workManager.enqueueUniquePeriodicWork(
162                        RECURRING_BACKUP, ExistingPeriodicWorkPolicy.UPDATE, periodicWorkRequest);
163            }
164        }
165    }
166
167    @Override
168    public void onStart() {
169        super.onStart();
170        requireActivity().setTitle(R.string.backup);
171    }
172
173    private boolean onBackupPreferenceClicked(final Preference preference) {
174        new AlertDialog.Builder(requireActivity())
175            .setTitle("Disable accounts")
176            .setMessage("Do you want to disable your accounts before making a backup (recommended)?")
177            .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
178                for (final var account : requireService().getAccounts()) {
179                    account.setOption(Account.OPTION_DISABLED, true);
180                    if (!requireService().updateAccount(account)) {
181                        Toast.makeText(requireActivity(), R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
182                    }
183                }
184                aboutToStartOneOffBackup();
185            })
186            .setNegativeButton(R.string.no, (dialog, whichButton) -> aboutToStartOneOffBackup()).show();
187        return true;
188    }
189
190    private void aboutToStartOneOffBackup() {
191        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
192            if (ContextCompat.checkSelfPermission(
193                            requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
194                    != PackageManager.PERMISSION_GRANTED) {
195                requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
196            } else {
197                startOneOffBackup();
198            }
199        } else {
200            startOneOffBackup();
201        }
202    }
203
204    private void startOneOffBackup() {
205        final OneTimeWorkRequest exportBackupWorkRequest =
206                new OneTimeWorkRequest.Builder(ExportBackupWorker.class)
207                        .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
208                        .build();
209        WorkManager.getInstance(requireContext())
210                .enqueueUniqueWork(
211                        CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
212        final MaterialAlertDialogBuilder builder =
213                new MaterialAlertDialogBuilder(requireActivity());
214        builder.setMessage(R.string.backup_started_message);
215        builder.setPositiveButton(R.string.ok, null);
216        builder.create().show();
217    }
218
219    private boolean onExportClicked(final Preference preference) {
220        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
221            if (ContextCompat.checkSelfPermission(
222                            requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
223                    != PackageManager.PERMISSION_GRANTED) {
224                requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
225            } else {
226                startExport();
227            }
228        } else {
229            startExport();
230        }
231        return true;
232    }
233
234    private void startExport() {
235        final OneTimeWorkRequest exportBackupWorkRequest =
236                new OneTimeWorkRequest.Builder(com.cheogram.android.ExportBackupService.class)
237                        .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
238                        .build();
239        WorkManager.getInstance(requireContext())
240                .enqueueUniqueWork(
241                        CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
242        final MaterialAlertDialogBuilder builder =
243                new MaterialAlertDialogBuilder(requireActivity());
244        builder.setMessage(R.string.backup_started_message);
245        builder.setPositiveButton(R.string.ok, null);
246        builder.create().show();
247    }
248}