Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setting for users to determine frequency of backup creation #13199

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -49,6 +49,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";
Expand All @@ -71,8 +72,9 @@ public final class SettingsValues extends SignalStoreValues {
private static final String KEEP_MUTED_CHATS_ARCHIVED = "settings.keepMutedChatsArchived";
private static final String USE_COMPACT_NAVIGATION_BAR = "settings.useCompactNavigationBar";

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<String> onConfigurationSettingChanged = new SingleLiveEvent<>();

Expand All @@ -90,7 +92,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);
}
}

Expand Down Expand Up @@ -273,6 +275,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);
}
Expand All @@ -281,7 +287,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);
}
Expand Down
Expand Up @@ -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)
}
}

Expand Down
@@ -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
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
Expand Up @@ -46,9 +46,10 @@ 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 = now.withHour(hour).withMinute(minute).withSecond(0);
LocalDateTime next = now.withHour(hour).withMinute(minute).withSecond(0).plusDays(freq);

int jitter = (new Random().nextInt(BACKUP_JITTER_WINDOW_SECONDS)) - (BACKUP_JITTER_WINDOW_SECONDS / 2);

Expand Down
Expand Up @@ -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";

Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -717,7 +717,16 @@
<string name="BackupsPreferenceFragment_signal_requires_external_storage_permission_in_order_to_create_backups">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\".</string>
<!-- Title of dialog shown when picking the time to perform a chat backup -->
<string name="BackupsPreferenceFragment__set_backup_time">Set backup time</string>
<string name="BackupsPreferenceFragment__time_label_daily">%1$s every day</string>
<plurals name="BackupsPreferenceFragment__time_label_n_days">
<item quantity="other">%1$s every %2$d days</item>
</plurals>


<!-- BackupFrequencyPickerDialogFragment -->
<string name="BackupFrequencyPickerDialogFragment__enter_frequency">Enter frequency (days)</string>
<string name="BackupFrequencyPickerDialogFragment__ok">OK</string>
<string name="BackupFrequencyPickerDialogFragment__cancel">Cancel</string>

<!-- CustomDefaultPreference -->
<string name="CustomDefaultPreference_using_custom">Using custom: %s</string>
Expand Down