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