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}