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.core.content.ContextCompat;
 16import androidx.preference.ListPreference;
 17import androidx.preference.Preference;
 18import androidx.work.Constraints;
 19import androidx.work.Data;
 20import androidx.work.ExistingPeriodicWorkPolicy;
 21import androidx.work.ExistingWorkPolicy;
 22import androidx.work.OneTimeWorkRequest;
 23import androidx.work.OutOfQuotaPolicy;
 24import androidx.work.PeriodicWorkRequest;
 25import androidx.work.WorkManager;
 26import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 27import com.google.common.base.Strings;
 28import com.google.common.primitives.Longs;
 29import eu.siacs.conversations.AppSettings;
 30import eu.siacs.conversations.Config;
 31import eu.siacs.conversations.R;
 32import eu.siacs.conversations.worker.ExportBackupWorker;
 33import java.util.concurrent.TimeUnit;
 34
 35public class BackupSettingsFragment extends XmppPreferenceFragment {
 36
 37    public static final String CREATE_ONE_OFF_BACKUP = "create_one_off_backup";
 38    private static final String RECURRING_BACKUP = "recurring_backup";
 39
 40    private final ActivityResultLauncher<String> requestStorageForBackupLauncher =
 41            registerForActivityResult(
 42                    new ActivityResultContracts.RequestPermission(),
 43                    isGranted -> {
 44                        if (isGranted) {
 45                            startOneOffBackup();
 46                        } else {
 47                            Toast.makeText(
 48                                            requireActivity(),
 49                                            getString(
 50                                                    R.string.no_storage_permission,
 51                                                    getString(R.string.app_name)),
 52                                            Toast.LENGTH_LONG)
 53                                    .show();
 54                        }
 55                    });
 56
 57    private final ActivityResultLauncher<Uri> pickBackupLocationLauncher =
 58            registerForActivityResult(
 59                    new ActivityResultContracts.OpenDocumentTree(),
 60                    uri -> {
 61                        if (uri == null) {
 62                            Log.d(Config.LOGTAG, "no backup location selected");
 63                            return;
 64                        }
 65                        submitBackupLocationPreference(uri);
 66                    });
 67
 68    @Override
 69    public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
 70        setPreferencesFromResource(R.xml.preferences_backup, rootKey);
 71        final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP);
 72        final ListPreference recurringBackup = findPreference(RECURRING_BACKUP);
 73        final var backupLocation = findPreference(AppSettings.BACKUP_LOCATION);
 74        if (createOneOffBackup == null || recurringBackup == null || backupLocation == null) {
 75            throw new IllegalStateException(
 76                    "The preference resource file is missing some preferences");
 77        }
 78        final var appSettings = new AppSettings(requireContext());
 79        backupLocation.setSummary(
 80                getString(
 81                        R.string.pref_create_backup_summary,
 82                        appSettings.getBackupLocationAsPath()));
 83        backupLocation.setOnPreferenceClickListener(this::onBackupLocationPreferenceClicked);
 84        createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
 85        setValues(
 86                recurringBackup,
 87                R.array.recurring_backup_values,
 88                value -> timeframeValueToName(requireContext(), value));
 89    }
 90
 91    private boolean onBackupLocationPreferenceClicked(final Preference preference) {
 92        this.pickBackupLocationLauncher.launch(null);
 93        return false;
 94    }
 95
 96    private void submitBackupLocationPreference(final Uri uri) {
 97        final var contentResolver = requireContext().getContentResolver();
 98        contentResolver.takePersistableUriPermission(
 99                uri,
100                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
101        final var appSettings = new AppSettings(requireContext());
102        appSettings.setBackupLocation(uri);
103        final var preference = findPreference(AppSettings.BACKUP_LOCATION);
104        if (preference == null) {
105            return;
106        }
107        preference.setSummary(
108                getString(R.string.pref_create_backup_summary, AppSettings.asPath(uri)));
109    }
110
111    @Override
112    protected void onSharedPreferenceChanged(@NonNull String key) {
113        super.onSharedPreferenceChanged(key);
114        if (RECURRING_BACKUP.equals(key)) {
115            final var sharedPreferences = getPreferenceManager().getSharedPreferences();
116            if (sharedPreferences == null) {
117                return;
118            }
119            final Long recurringBackupInterval =
120                    Longs.tryParse(
121                            Strings.nullToEmpty(
122                                    sharedPreferences.getString(RECURRING_BACKUP, null)));
123            if (recurringBackupInterval == null) {
124                return;
125            }
126            Log.d(
127                    Config.LOGTAG,
128                    "recurring backup interval changed to: " + recurringBackupInterval);
129            final var workManager = WorkManager.getInstance(requireContext());
130            if (recurringBackupInterval <= 0) {
131                workManager.cancelUniqueWork(RECURRING_BACKUP);
132            } else {
133                final Constraints constraints =
134                        new Constraints.Builder()
135                                .setRequiresBatteryNotLow(true)
136                                .setRequiresStorageNotLow(true)
137                                .build();
138
139                final PeriodicWorkRequest periodicWorkRequest =
140                        new PeriodicWorkRequest.Builder(
141                                        ExportBackupWorker.class,
142                                        recurringBackupInterval,
143                                        TimeUnit.SECONDS)
144                                .setConstraints(constraints)
145                                .setInputData(
146                                        new Data.Builder()
147                                                .putBoolean("recurring_backup", true)
148                                                .build())
149                                .build();
150                workManager.enqueueUniquePeriodicWork(
151                        RECURRING_BACKUP, ExistingPeriodicWorkPolicy.UPDATE, periodicWorkRequest);
152            }
153        }
154    }
155
156    @Override
157    public void onStart() {
158        super.onStart();
159        requireActivity().setTitle(R.string.backup);
160    }
161
162    private boolean onBackupPreferenceClicked(final Preference preference) {
163        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
164            if (ContextCompat.checkSelfPermission(
165                            requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
166                    != PackageManager.PERMISSION_GRANTED) {
167                requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
168            } else {
169                startOneOffBackup();
170            }
171        } else {
172            startOneOffBackup();
173        }
174        return true;
175    }
176
177    private void startOneOffBackup() {
178        final OneTimeWorkRequest exportBackupWorkRequest =
179                new OneTimeWorkRequest.Builder(ExportBackupWorker.class)
180                        .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
181                        .build();
182        WorkManager.getInstance(requireContext())
183                .enqueueUniqueWork(
184                        CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
185        final MaterialAlertDialogBuilder builder =
186                new MaterialAlertDialogBuilder(requireActivity());
187        builder.setMessage(R.string.backup_started_message);
188        builder.setPositiveButton(R.string.ok, null);
189        builder.create().show();
190    }
191}