Skip to content

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

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

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e4a1e69
Add backup frequency as a setting that can be set
May 10, 2023
e85ebb4
Show dialog to select backup frequency
May 10, 2023
79c0f26
todo commit
May 10, 2023
dd99598
Use frequency setting to schedule future backups
May 12, 2023
bb1be2c
Merge branch 'main' into slowbackups
May 12, 2023
9aa3d05
Pre-select backup frequency choice with user's current setting
May 12, 2023
dbb9390
Rename backup frequency picker dialog class for accuracy
Jul 30, 2023
8a01eb1
Merge branch 'main' into slowbackups
Jul 30, 2023
9c4d5d3
Show backup frequency in app settings
Jul 30, 2023
4b19f99
Improve backup frequency picker buttons
Jul 30, 2023
3031c6c
Minor cleanups to backup frequency stuff
Jul 30, 2023
92a448f
Merge branch 'main' into slowbackups
Oct 1, 2023
1745027
Add docstring for the unit of next backup time
Oct 2, 2023
5bea670
Schedule next backup relative to last backup when changing frequency
Oct 2, 2023
515a5b4
Merge branch 'main' into slowbackups
Dec 3, 2023
509c14e
Convert raw strings to resources in backup settings
Dec 3, 2023
05e82fc
Add newline to end of file
Dec 3, 2023
d57b50a
Convert string to plurals for non-English language
Dec 4, 2023
9309712
Merge branch 'main' into slowbackups
Jan 3, 2024
28f72a4
Merge branch 'main' into slowbackups
Jan 11, 2024
194df22
Merge branch 'main' into slowbackups
Mar 12, 2024
18a2bc1
Merge branch 'main' into slowbackups
Apr 15, 2024
8eba4bd
Merge branch 'main' into slowbackups
Jun 25, 2024
4032a70
Fix slow backups (farewelltospring's version)
Jun 25, 2024
175ad4d
Merge branch 'main' into slowbackups
Aug 27, 2024
db926b0
Merge branch 'main' into slowbackups
Oct 17, 2024
369c8bb
Merge branch 'main' into slowbackups
Jan 5, 2025
49ff11a
Merge branch 'main' into slowbackups
Feb 17, 2025
293ceff
Add a 'never' option for backup frequency, new UI
Feb 17, 2025
3824961
Do not show frequency setting when backups are disabled
Feb 17, 2025
ee4c713
More formatting :(
Feb 17, 2025
0093a68
Merge branch 'main' into slowbackups
Apr 21, 2025
54ed0b2
Add date label when backups are known to be enabled
Apr 22, 2025
ce82f6d
Merge branch 'main' into slowbackups
Apr 28, 2025
76a86f8
Rename backup frequency picker for consistency with backup time picker
Apr 28, 2025
e077943
Cancel system alarm for backups when disabling backup frequency
May 8, 2025
ba49dc4
Cancel ongoing backup jobs when the backup frequency is set to never
May 8, 2025
2990158
Remove inaccurate/obsolete comment
May 8, 2025
357ac9c
Rename function to apply new backup schedule setting for accuracy
May 8, 2025
90c3eb7
Merge branch 'main' into slowbackups
Jun 8, 2025
b7ba9bd
Merge branch 'main' into slowbackups
Jun 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ public static void enqueueArchive() {
jobManager.add(new LocalArchiveJob(parameters.build()));
}

/** Sends a cancellation signal to all ongoing backup jobs. */
public static void cancelRunningJobs() {
JobManager jobManager = AppDependencies.getJobManager();
jobManager.cancelAllInQueue(QUEUE);
}

private LocalBackupJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mms.SentMediaQuality;
import org.thoughtcrime.securesms.preferences.BackupFrequencyV1;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
Expand Down Expand Up @@ -45,6 +46,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";
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 Down Expand Up @@ -73,8 +75,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 BackupFrequencyV1 BACKUP_DEFAULT_FREQUENCY = BackupFrequencyV1.MONTHLY;
public static final int BACKUP_DEFAULT_HOUR = 2;
public static final int BACKUP_DEFAULT_MINUTE = 0;

private final SingleLiveEvent<String> onConfigurationSettingChanged = new SingleLiveEvent<>();

Expand Down Expand Up @@ -106,7 +109,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 @@ -307,6 +310,10 @@ public void setBackupEnabled(boolean backupEnabled) {
putBoolean(BACKUPS_ENABLED, backupEnabled);
}

public BackupFrequencyV1 getBackupFrequency() {
return BackupFrequencyV1.valueOf(getString(BACKUPS_SCHEDULE_FREQUENCY, BACKUP_DEFAULT_FREQUENCY.name()));
}

public int getBackupHour() {
return getInteger(BACKUPS_SCHEDULE_HOUR, BACKUP_DEFAULT_HOUR);
}
Expand All @@ -315,7 +322,8 @@ public int getBackupMinute() {
return getInteger(BACKUPS_SCHEDULE_MINUTE, BACKUP_DEFAULT_MINUTE);
}

public void setBackupSchedule(int hour, int minute) {
public void setBackupSchedule(BackupFrequencyV1 frequency, int hour, int minute) {
putString(BACKUPS_SCHEDULE_FREQUENCY, frequency.name());
putInteger(BACKUPS_SCHEDULE_HOUR, hour);
putInteger(BACKUPS_SCHEDULE_MINUTE, minute);
}
Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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: BackupFrequencyV1) : DialogFragment() {

private val frequencyOptions = BackupFrequencyV1.entries
private var selectedFrequency: BackupFrequencyV1 = defaultFrequency
private var callback: OnClickListener? = null

override fun onCreateDialog(savedInstance: Bundle?): Dialog {
val context = requireContext()
val localizedFrequencyOptions = frequencyOptions
.map { it.getResourceId() }
.map { context.getString(it) }
.toTypedArray()
val defaultIndex = frequencyOptions.indexOf(defaultFrequency)

return MaterialAlertDialogBuilder(context)
.setSingleChoiceItems(localizedFrequencyOptions, defaultIndex) { _, selectedIndex ->
selectedFrequency = frequencyOptions[selectedIndex]
}
.setTitle(R.string.BackupFrequencyPickerDialogFragment__set_backup_frequency)
.setPositiveButton(R.string.BackupFrequencyPickerDialogFragment__ok, callback)
.setNegativeButton(R.string.BackupFrequencyPickerDialogFragment__cancel, null)
.create()
}

fun getValue(): BackupFrequencyV1 = selectedFrequency

fun setOnPositiveButtonClickListener(cb: OnClickListener) {
this.callback = cb
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.thoughtcrime.securesms.preferences

import androidx.annotation.StringRes
import org.thoughtcrime.securesms.R

enum class BackupFrequencyV1(val days: Int) {
DAILY(1),
WEEKLY(7),
MONTHLY(30),
QUARTERLY(90),
NEVER(999);

@StringRes
fun getResourceId(): Int {
return when (this) {
DAILY -> R.string.BackupsPreferenceFragment__frequency_label_daily
WEEKLY -> R.string.BackupsPreferenceFragment__frequency_label_weekly
MONTHLY -> R.string.BackupsPreferenceFragment__frequency_label_monthly
QUARTERLY -> R.string.BackupsPreferenceFragment__frequency_label_quarterly
NEVER -> R.string.BackupsPreferenceFragment__frequency_label_never
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import java.time.LocalTime;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;

public class BackupsPreferenceFragment extends Fragment {

Expand All @@ -60,7 +61,9 @@ public class BackupsPreferenceFragment extends Fragment {
private View folder;
private View verify;
private View timer;
private View frequencyView;
private TextView timeLabel;
private TextView frequencyLabel;
private TextView toggle;
private TextView info;
private TextView summary;
Expand All @@ -85,6 +88,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
verify = view.findViewById(R.id.fragment_backup_verify);
timer = view.findViewById(R.id.fragment_backup_time);
timeLabel = view.findViewById(R.id.fragment_backup_time_value);
frequencyView = view.findViewById(R.id.fragment_backup_frequency);
frequencyLabel = view.findViewById(R.id.fragment_backup_frequency_value);
toggle = view.findViewById(R.id.fragment_backup_toggle);
info = view.findViewById(R.id.fragment_backup_info);
summary = view.findViewById(R.id.fragment_backup_create_summary);
Expand All @@ -96,6 +101,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
create.setOnClickListener(unused -> onCreateClicked());
verify.setOnClickListener(unused -> BackupDialog.showVerifyBackupPassphraseDialog(requireContext()));
timer.setOnClickListener(unused -> pickTime());
frequencyView.setOnClickListener(unused -> pickFrequency());

formatter.setMinimumFractionDigits(1);
formatter.setMaximumFractionDigits(1);
Expand Down Expand Up @@ -275,16 +281,52 @@ private void pickTime() {
.setTitleText(R.string.BackupsPreferenceFragment__set_backup_time)
.build();
timePickerFragment.addOnPositiveButtonClickListener(v -> {
BackupFrequencyV1 frequency = SignalStore.settings().getBackupFrequency();
int hour = timePickerFragment.getHour();
int minute = timePickerFragment.getMinute();
SignalStore.settings().setBackupSchedule(hour, minute);
applyNewBackupScheduleSetting(frequency, hour, minute);
updateTimeLabel();
TextSecurePreferences.setNextBackupTime(requireContext(), 0);
LocalBackupListener.schedule(requireContext());
});
timePickerFragment.show(getChildFragmentManager(), "TIME_PICKER");
}

private void pickFrequency() {
final BackupFrequencyPickerDialogFragment frequencyPickerDialogFragment = new BackupFrequencyPickerDialogFragment(SignalStore.settings().getBackupFrequency());
frequencyPickerDialogFragment.setOnPositiveButtonClickListener((unused1, unused2) -> {
BackupFrequencyV1 frequency = frequencyPickerDialogFragment.getValue();
int hour = SignalStore.settings().getBackupHour();
int minute = SignalStore.settings().getBackupMinute();
applyNewBackupScheduleSetting(frequency, hour, minute);
updateDateLabel();
});
frequencyPickerDialogFragment.show(getChildFragmentManager(), "FREQUENCY_PICKER");
}

/** Update the settings on disk and then schedule a backup.
*
* <p>This method should be called when the user presses the buttons to set a new backup schedule with the given parameters. */
private void applyNewBackupScheduleSetting(BackupFrequencyV1 frequency, int hour, int minute) {
Log.i(TAG, "Setting backup schedule: " + frequency.name() + " at" + hour + "h" + minute + "m");
SignalStore.settings().setBackupSchedule(frequency, hour, minute);
if (frequency == BackupFrequencyV1.NEVER) {
LocalBackupListener.unschedule(requireContext());
} else {
// 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 + frequency.getDays() * 24 * 60 * 60 * 1000L);
LocalBackupListener.schedule(requireContext());
}
}

private void onCreateClickedLegacy() {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
Expand All @@ -304,6 +346,10 @@ private void updateTimeLabel() {
timeLabel.setText(JavaTimeExtensionsKt.formatHours(time, requireContext()));
}

private void updateDateLabel() {
frequencyLabel.setText(getResources().getString(SignalStore.settings().getBackupFrequency().getResourceId()));
}

private void updateToggle() {
boolean userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.getApplication()) || !SignalStore.account().isRegistered();
boolean clientDeprecated = SignalStore.misc().isClientDeprecated();
Expand All @@ -317,8 +363,10 @@ private void setBackupsEnabled() {
create.setVisibility(View.VISIBLE);
verify.setVisibility(View.VISIBLE);
timer.setVisibility(View.VISIBLE);
frequencyView.setVisibility(View.VISIBLE);
updateToggle();
updateTimeLabel();
updateDateLabel();
setBackupFolderName();
}

Expand All @@ -328,6 +376,7 @@ private void setBackupsDisabled() {
folder.setVisibility(View.GONE);
verify.setVisibility(View.GONE);
timer.setVisibility(View.GONE);
frequencyView.setVisibility(View.GONE);
updateToggle();
AppDependencies.getJobManager().cancelAllInQueue(LocalBackupJob.QUEUE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import org.thoughtcrime.securesms.jobs.LocalBackupJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.preferences.BackupFrequencyV1;
import org.thoughtcrime.securesms.util.JavaTimeExtensionsKt;
import org.thoughtcrime.securesms.util.TextSecurePreferences;

Expand All @@ -30,25 +31,39 @@ protected long getNextScheduledExecutionTime(Context context) {

@Override
protected long onAlarm(Context context, long scheduledTime) {
if (SignalStore.settings().isBackupEnabled()) {
if (SignalStore.settings().isBackupEnabled() && SignalStore.settings().getBackupFrequency() != BackupFrequencyV1.NEVER) {
LocalBackupJob.enqueue(false);
}

return setNextBackupTimeToIntervalFromNow(context);
}

public static void schedule(Context context) {
if (SignalStore.settings().isBackupEnabled()) {
if (SignalStore.settings().isBackupEnabled() && SignalStore.settings().getBackupFrequency() != BackupFrequencyV1.NEVER) {
new LocalBackupListener().onReceive(context, getScheduleIntent());
}
}

/** Cancels any future backup scheduled with AlarmManager and attempts to cancel any ongoing backup job. */
public static void unschedule(Context context) {
new LocalBackupListener().cancel(context);
LocalBackupJob.cancelRunningJobs();
}

public static long setNextBackupTimeToIntervalFromNow(@NonNull Context context) {
LocalDateTime now = LocalDateTime.now();
int hour = SignalStore.settings().getBackupHour();
int minute = SignalStore.settings().getBackupMinute();
LocalDateTime next = MessageBackupListener.getNextDailyBackupTimeFromNowWithJitter(now, hour, minute, BACKUP_JITTER_WINDOW_SECONDS, new Random());
BackupFrequencyV1 freq = SignalStore.settings().getBackupFrequency();

if (freq == BackupFrequencyV1.NEVER) {
TextSecurePreferences.setNextBackupTime(context, -1);
return -1;
}

LocalDateTime now = LocalDateTime.now();
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.getDays());
long nextTime = JavaTimeExtensionsKt.toMillis(next);

TextSecurePreferences.setNextBackupTime(context, nextTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ public abstract class PersistentAlarmManagerListener extends BroadcastReceiver {

protected abstract long onAlarm(Context context, long scheduledTime);

public void cancel(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent(context, getClass());
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, PendingIntentFlags.immutable());

info("Cancelling alarm");
alarmManager.cancel(pendingIntent);
}

@Override
public void onReceive(Context context, Intent intent) {
info(String.format("onReceive(%s)", intent.getAction()));
Expand All @@ -47,6 +56,7 @@ public void onReceive(Context context, Intent intent) {
return;
}

// If we've already scheduled this alarm, cancel it so we can schedule it again with the new time.
alarmManager.cancel(pendingIntent);

if (shouldScheduleExact()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,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

@Deprecated
public static final String REGISTRATION_LOCK_PREF_V1 = "pref_registration_lock";
Expand Down
30 changes: 30 additions & 0 deletions app/src/main/res/layout/fragment_backups.xml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,36 @@
tools:text="3:00" />
</LinearLayout>

<LinearLayout
android:id="@+id/fragment_backup_frequency"
android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightLarge"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/BackupsPreferenceFragment__backup_frequency"
android:textAppearance="@style/Signal.Text.Body"
android:textColor="@color/signal_text_primary" />

<TextView
android:id="@+id/fragment_backup_frequency_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/Signal.Text.Preview"
android:textColor="@color/signal_text_secondary"
tools:text="3:00" />
</LinearLayout>

<LinearLayout
android:id="@+id/fragment_backup_verify"
android:layout_width="match_parent"
Expand Down
Loading