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}