1package eu.siacs.conversations.ui.fragment.settings;
2
3import android.Manifest;
4import android.content.pm.PackageManager;
5import android.os.Build;
6import android.os.Bundle;
7import android.util.Log;
8import android.widget.Toast;
9
10import androidx.activity.result.ActivityResultLauncher;
11import androidx.activity.result.contract.ActivityResultContracts;
12import androidx.annotation.NonNull;
13import androidx.annotation.Nullable;
14import androidx.appcompat.app.AlertDialog;
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;
26
27import com.google.android.material.dialog.MaterialAlertDialogBuilder;
28import com.google.common.base.Strings;
29import com.google.common.primitives.Longs;
30
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;
36
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 @Override
62 public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
63 setPreferencesFromResource(R.xml.preferences_backup, rootKey);
64 final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP);
65 final var export = findPreference("export");
66 final ListPreference recurringBackup = findPreference(RECURRING_BACKUP);
67 final var backupDirectory = findPreference("backup_directory");
68 if (createOneOffBackup == null || recurringBackup == null || backupDirectory == null) {
69 throw new IllegalStateException(
70 "The preference resource file is missing some preferences");
71 }
72 backupDirectory.setSummary(
73 getString(
74 R.string.pref_create_backup_summary,
75 FileBackend.getBackupDirectory(requireContext()).getAbsolutePath()));
76 createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked);
77 export.setOnPreferenceClickListener(this::onExportClicked);
78 setValues(
79 recurringBackup,
80 R.array.recurring_backup_values,
81 value -> timeframeValueToName(requireContext(), value));
82 }
83
84 @Override
85 protected void onSharedPreferenceChanged(@NonNull String key) {
86 super.onSharedPreferenceChanged(key);
87 if (RECURRING_BACKUP.equals(key)) {
88 final var sharedPreferences = getPreferenceManager().getSharedPreferences();
89 if (sharedPreferences == null) {
90 return;
91 }
92 final Long recurringBackupInterval =
93 Longs.tryParse(
94 Strings.nullToEmpty(
95 sharedPreferences.getString(RECURRING_BACKUP, null)));
96 if (recurringBackupInterval == null) {
97 return;
98 }
99 Log.d(
100 Config.LOGTAG,
101 "recurring backup interval changed to: " + recurringBackupInterval);
102 final var workManager = WorkManager.getInstance(requireContext());
103 if (recurringBackupInterval <= 0) {
104 workManager.cancelUniqueWork(RECURRING_BACKUP);
105 } else {
106 final Constraints constraints =
107 new Constraints.Builder()
108 .setRequiresBatteryNotLow(true)
109 .setRequiresStorageNotLow(true)
110 .build();
111
112 final PeriodicWorkRequest periodicWorkRequest =
113 new PeriodicWorkRequest.Builder(
114 ExportBackupWorker.class,
115 recurringBackupInterval,
116 TimeUnit.SECONDS)
117 .setConstraints(constraints)
118 .setInputData(
119 new Data.Builder()
120 .putBoolean("recurring_backup", true)
121 .build())
122 .build();
123 workManager.enqueueUniquePeriodicWork(
124 RECURRING_BACKUP, ExistingPeriodicWorkPolicy.UPDATE, periodicWorkRequest);
125 }
126 }
127 }
128
129 @Override
130 public void onStart() {
131 super.onStart();
132 requireActivity().setTitle(R.string.backup);
133 }
134
135 private boolean onBackupPreferenceClicked(final Preference preference) {
136 new AlertDialog.Builder(requireActivity())
137 .setTitle("Disable accounts")
138 .setMessage("Do you want to disable your accounts before making a backup (recommended)?")
139 .setPositiveButton(R.string.yes, (dialog, whichButton) -> {
140 for (final var account : requireService().getAccounts()) {
141 account.setOption(Account.OPTION_DISABLED, true);
142 if (!requireService().updateAccount(account)) {
143 Toast.makeText(requireActivity(), R.string.unable_to_update_account, Toast.LENGTH_SHORT).show();
144 }
145 }
146 aboutToStartOneOffBackup();
147 })
148 .setNegativeButton(R.string.no, (dialog, whichButton) -> aboutToStartOneOffBackup()).show();
149 return true;
150 }
151
152 private void aboutToStartOneOffBackup() {
153 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
154 if (ContextCompat.checkSelfPermission(
155 requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
156 != PackageManager.PERMISSION_GRANTED) {
157 requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
158 } else {
159 startOneOffBackup();
160 }
161 } else {
162 startOneOffBackup();
163 }
164 }
165
166 private void startOneOffBackup() {
167 final OneTimeWorkRequest exportBackupWorkRequest =
168 new OneTimeWorkRequest.Builder(ExportBackupWorker.class)
169 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
170 .build();
171 WorkManager.getInstance(requireContext())
172 .enqueueUniqueWork(
173 CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
174 final MaterialAlertDialogBuilder builder =
175 new MaterialAlertDialogBuilder(requireActivity());
176 builder.setMessage(R.string.backup_started_message);
177 builder.setPositiveButton(R.string.ok, null);
178 builder.create().show();
179 }
180
181 private boolean onExportClicked(final Preference preference) {
182 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
183 if (ContextCompat.checkSelfPermission(
184 requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
185 != PackageManager.PERMISSION_GRANTED) {
186 requestStorageForBackupLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
187 } else {
188 startExport();
189 }
190 } else {
191 startExport();
192 }
193 return true;
194 }
195
196 private void startExport() {
197 final OneTimeWorkRequest exportBackupWorkRequest =
198 new OneTimeWorkRequest.Builder(com.cheogram.android.ExportBackupService.class)
199 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
200 .build();
201 WorkManager.getInstance(requireContext())
202 .enqueueUniqueWork(
203 CREATE_ONE_OFF_BACKUP, ExistingWorkPolicy.KEEP, exportBackupWorkRequest);
204 final MaterialAlertDialogBuilder builder =
205 new MaterialAlertDialogBuilder(requireActivity());
206 builder.setMessage(R.string.backup_started_message);
207 builder.setPositiveButton(R.string.ok, null);
208 builder.create().show();
209 }
210}