diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java index e89e6dc2b1b..1a6bb5752f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -45,6 +45,7 @@ public final class SettingsValues extends SignalStoreValues { public static final String PREFER_SYSTEM_EMOJI = "settings.use.system.emoji"; public static final String ENTER_KEY_SENDS = "settings.enter.key.sends"; public static final String BACKUPS_ENABLED = "settings.backups.enabled"; + public static final String BACKUPS_SCHEDULE_FREQUENCY = "settings.backups.schedule.frequency"; // days public static final String BACKUPS_SCHEDULE_HOUR = "settings.backups.schedule.hour"; public static final String BACKUPS_SCHEDULE_MINUTE = "settings.backups.schedule.minute"; public static final String SMS_DELIVERY_REPORTS_ENABLED = "settings.sms.delivery.reports.enabled"; @@ -73,8 +74,9 @@ public final class SettingsValues extends SignalStoreValues { private static final String SCREEN_LOCK_ENABLED = "settings.screen.lock.enabled"; private static final String SCREEN_LOCK_TIMEOUT = "settings.screen.lock.timeout"; - public static final int BACKUP_DEFAULT_HOUR = 2; - public static final int BACKUP_DEFAULT_MINUTE = 0; + public static final int BACKUP_DEFAULT_FREQUENCY = 30; // days + public static final int BACKUP_DEFAULT_HOUR = 2; + public static final int BACKUP_DEFAULT_MINUTE = 0; private final SingleLiveEvent onConfigurationSettingChanged = new SingleLiveEvent<>(); @@ -106,7 +108,7 @@ void onFirstEverAppLaunch() { } if (!store.containsKey(BACKUPS_SCHEDULE_HOUR)) { // Initialize backup time to a 5min interval between 1-5am - setBackupSchedule(new Random().nextInt(5) + 1, new Random().nextInt(12) * 5); + setBackupSchedule(BACKUP_DEFAULT_FREQUENCY, new Random().nextInt(5) + 1, new Random().nextInt(12) * 5); } } @@ -307,6 +309,10 @@ public void setBackupEnabled(boolean backupEnabled) { putBoolean(BACKUPS_ENABLED, backupEnabled); } + public int getBackupFrequency() { + return getInteger(BACKUPS_SCHEDULE_FREQUENCY, BACKUP_DEFAULT_FREQUENCY); + } + public int getBackupHour() { return getInteger(BACKUPS_SCHEDULE_HOUR, BACKUP_DEFAULT_HOUR); } @@ -315,7 +321,8 @@ public int getBackupMinute() { return getInteger(BACKUPS_SCHEDULE_MINUTE, BACKUP_DEFAULT_MINUTE); } - public void setBackupSchedule(int hour, int minute) { + public void setBackupSchedule(int days, int hour, int minute) { + putInteger(BACKUPS_SCHEDULE_FREQUENCY, days); putInteger(BACKUPS_SCHEDULE_HOUR, hour); putInteger(BACKUPS_SCHEDULE_MINUTE, minute); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupJitterMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupJitterMigrationJob.kt index c4aca674bcc..f650d0c125d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupJitterMigrationJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/BackupJitterMigrationJob.kt @@ -22,13 +22,14 @@ internal class BackupJitterMigrationJob(parameters: Parameters = Parameters.Buil override fun isUiBlocking(): Boolean = false override fun performMigration() { + val frequency = SignalStore.settings.backupFrequency val hour = SignalStore.settings.backupHour val minute = SignalStore.settings.backupMinute if (hour == SettingsValues.BACKUP_DEFAULT_HOUR && minute == SettingsValues.BACKUP_DEFAULT_MINUTE) { val rand = Random() val newHour = rand.nextInt(3) + 1 // between 1AM - 3AM val newMinute = rand.nextInt(12) * 5 // 5 minute intervals up to +55 minutes - SignalStore.settings.setBackupSchedule(newHour, newMinute) + SignalStore.settings.setBackupSchedule(frequency, newHour, newMinute) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupFrequencyPickerDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupFrequencyPickerDialogFragment.kt new file mode 100644 index 00000000000..2cebd0afb9a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupFrequencyPickerDialogFragment.kt @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.preferences + +import android.app.Dialog +import android.content.DialogInterface.OnClickListener +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder + +import org.thoughtcrime.securesms.R + +class BackupFrequencyPickerDialogFragment(private val defaultFrequency: Int) : DialogFragment() { + private val dayOptions = arrayOf("1", "7", "30", "90", "180", "365") + private var index: Int = 0 + private var callback: OnClickListener? = null + + override fun onCreateDialog(savedInstance: Bundle?): Dialog { + val defaultIndex = this.dayOptions.indexOf(this.defaultFrequency.toString()) // preselect the backup frequency choice if it's valid + this.index = defaultIndex + return MaterialAlertDialogBuilder(requireContext()) + .setSingleChoiceItems(this.dayOptions, defaultIndex) { _, i -> this.index = i } + .setTitle(R.string.BackupFrequencyPickerDialogFragment__enter_frequency) + .setPositiveButton(R.string.BackupFrequencyPickerDialogFragment__ok, this.callback) + .setNegativeButton(R.string.BackupFrequencyPickerDialogFragment__cancel, null) + .create() + } + + fun getValue(): Int = this.dayOptions[this.index].toInt() + + fun setOnPositiveButtonClickListener(cb: OnClickListener) { + this.callback = cb + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java index 07f8ec95d24..1e92e71c886 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BackupsPreferenceFragment.java @@ -2,12 +2,14 @@ import android.Manifest; import android.app.Activity; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.format.DateFormat; import android.text.method.LinkMovementMethod; +import android.util.TimeUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -46,6 +48,7 @@ import java.time.LocalTime; import java.util.Locale; import java.util.Objects; +import java.util.Optional; public class BackupsPreferenceFragment extends Fragment { @@ -259,22 +262,40 @@ private void onCreateClickedApi29() { } private void pickTime() { - int timeFormat = DateFormat.is24HourFormat(requireContext()) ? TimeFormat.CLOCK_24H : TimeFormat.CLOCK_12H; - final MaterialTimePicker timePickerFragment = new MaterialTimePicker.Builder() - .setTimeFormat(timeFormat) - .setHour(SignalStore.settings().getBackupHour()) - .setMinute(SignalStore.settings().getBackupMinute()) - .setTitleText(R.string.BackupsPreferenceFragment__set_backup_time) - .build(); - timePickerFragment.addOnPositiveButtonClickListener(v -> { - int hour = timePickerFragment.getHour(); - int minute = timePickerFragment.getMinute(); - SignalStore.settings().setBackupSchedule(hour, minute); - updateTimeLabel(); - TextSecurePreferences.setNextBackupTime(requireContext(), 0); - LocalBackupListener.schedule(requireContext()); + // User should select the backup frequency first, and then the time of day to do the backups. + final BackupFrequencyPickerDialogFragment frequencyPickerDialogFragment = new BackupFrequencyPickerDialogFragment(SignalStore.settings().getBackupFrequency()); + frequencyPickerDialogFragment.setOnPositiveButtonClickListener((unused1, unused2) -> { + int timeFormat = DateFormat.is24HourFormat(requireContext()) ? TimeFormat.CLOCK_24H : TimeFormat.CLOCK_12H; + final MaterialTimePicker timePickerFragment = new MaterialTimePicker.Builder() + .setTimeFormat(timeFormat) + .setHour(SignalStore.settings().getBackupHour()) + .setMinute(SignalStore.settings().getBackupMinute()) + .setTitleText(R.string.BackupsPreferenceFragment__set_backup_time) + .build(); + timePickerFragment.addOnPositiveButtonClickListener(v -> { + int days = frequencyPickerDialogFragment.getValue(); + int hour = timePickerFragment.getHour(); + int minute = timePickerFragment.getMinute(); + Log.i(TAG, "Setting backup schedule: every " + days + " days at" + hour + "h" + minute + "m"); + SignalStore.settings().setBackupSchedule(days, hour, minute); + updateTimeLabel(); + // Schedule the next backup using the newly set frequency, but relative to the time of the + // last backup. This should only kick off a new backup to be created immediately if the + // last backup was long enough ago (or doesn't exist at all). + long lastBackupTime = 0; + try { + lastBackupTime = Optional.ofNullable(BackupUtil.getLatestBackup()) + .map(BackupUtil.BackupInfo::getTimestamp) + .orElse(0L); + } catch (NoExternalStorageException ignored) {} + TextSecurePreferences.setNextBackupTime(requireContext(), lastBackupTime + days * 24 * 60 * 60 * 1000L); + LocalBackupListener.schedule(requireContext()); + }); + + timePickerFragment.show(getChildFragmentManager(), "TIME_PICKER"); }); - timePickerFragment.show(getChildFragmentManager(), "TIME_PICKER"); + + frequencyPickerDialogFragment.show(getChildFragmentManager(), "FREQUENCY_PICKER"); } private void onCreateClickedLegacy() { @@ -290,10 +311,17 @@ private void onCreateClickedLegacy() { } private void updateTimeLabel() { - final int backupHour = SignalStore.settings().getBackupHour(); - final int backupMinute = SignalStore.settings().getBackupMinute(); - LocalTime time = LocalTime.of(backupHour, backupMinute); - timeLabel.setText(JavaTimeExtensionsKt.formatHours(time, requireContext())); + final int backupFrequency = SignalStore.settings().getBackupFrequency(); + final int backupHour = SignalStore.settings().getBackupHour(); + final int backupMinute = SignalStore.settings().getBackupMinute(); + LocalTime time = LocalTime.of(backupHour, backupMinute); + + String backupTimeString = JavaTimeExtensionsKt.formatHours(time, requireContext()); + timeLabel.setText(backupFrequency == 1 ? getString(R.string.BackupsPreferenceFragment__time_label_daily, backupTimeString) + : getResources().getQuantityString(R.plurals.BackupsPreferenceFragment__time_label_n_days, + backupFrequency, + backupTimeString, backupFrequency) + ); } private void setBackupsEnabled() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java index f7888530fbd..d6a016bcf02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/LocalBackupListener.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.jobs.LocalBackupJob; +import org.thoughtcrime.securesms.keyvalue.SettingsValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.JavaTimeExtensionsKt; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -45,10 +46,12 @@ public static void schedule(Context context) { public static long setNextBackupTimeToIntervalFromNow(@NonNull Context context) { LocalDateTime now = LocalDateTime.now(); + int freq = SignalStore.settings().getBackupFrequency(); int hour = SignalStore.settings().getBackupHour(); int minute = SignalStore.settings().getBackupMinute(); LocalDateTime next = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, hour, minute, BACKUP_JITTER_WINDOW_SECONDS, new Random()); + next = next.plusDays(freq); long nextTime = JavaTimeExtensionsKt.toMillis(next); TextSecurePreferences.setNextBackupTime(context, nextTime); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 7482d75caa2..5f554b23309 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -127,7 +127,7 @@ public class TextSecurePreferences { public static final String BACKUP_ENABLED = "pref_backup_enabled"; private static final String BACKUP_PASSPHRASE = "pref_backup_passphrase"; private static final String ENCRYPTED_BACKUP_PASSPHRASE = "pref_encrypted_backup_passphrase"; - private static final String BACKUP_TIME = "pref_backup_next_time"; + private static final String BACKUP_TIME = "pref_backup_next_time"; // milliseconds since 1970 public static final String TRANSFER = "pref_transfer"; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c07ff8ddd15..7f0136e6bdd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -885,7 +885,16 @@ Signal requires external storage permission in order to create backups, but it has been permanently denied. Please continue to app settings, select \"Permissions\" and enable \"Storage\". Set backup time + %1$s every day + + %1$s every %2$d days + + + + Enter frequency (days) + OK + Cancel Using custom: %s