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}