BackupSettingsFragment.java

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