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}