Expose scheduling options in the UI

This commit is contained in:
Torsten Grote 2024-02-22 16:47:01 -03:00
parent f593b66e00
commit 4eaa806636
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
16 changed files with 287 additions and 65 deletions

View file

@ -13,6 +13,7 @@ import android.os.UserHandle
import android.os.UserManager
import android.provider.Settings
import androidx.work.WorkManager
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import com.stevesoltys.seedvault.crypto.cryptoModule
import com.stevesoltys.seedvault.header.headerModule
import com.stevesoltys.seedvault.metadata.MetadataManager
@ -56,7 +57,7 @@ open class App : Application() {
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
factory { AppListRetriever(this@App, get(), get(), get()) }
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get(), get()) }
viewModel { SettingsViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
@ -133,7 +134,9 @@ open class App : Application() {
if (!isFrameworkSchedulingEnabled()) return // already on own scheduling
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
if (backupManager.isBackupEnabled) AppBackupWorker.schedule(applicationContext)
if (backupManager.isBackupEnabled) {
AppBackupWorker.schedule(applicationContext, settingsManager, UPDATE)
}
// cancel old D2D worker
WorkManager.getInstance(this).cancelUniqueWork("APP_BACKUP")
}

View file

@ -23,12 +23,10 @@ import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_ST
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.koin.core.context.GlobalContext.get
import java.util.concurrent.TimeUnit.HOURS
import java.util.Date
private val TAG = UsbIntentReceiver::class.java.simpleName
private const val HOURS_AUTO_BACKUP: Long = 24
class UsbIntentReceiver : UsbMonitor() {
// using KoinComponent would crash robolectric tests :(
@ -43,11 +41,13 @@ class UsbIntentReceiver : UsbMonitor() {
return if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, checking backup time...")
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
if (backupMillis >= settingsManager.backupFrequencyInMillis) {
Log.d(TAG, "Last backup older than it should be, requesting a backup...")
Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}")
true
} else {
Log.d(TAG, "We have a recent backup, not requesting a new one.")
Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}")
false
}
} else {

View file

@ -16,10 +16,10 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by sharedViewModel()
private val packageService: PackageService by inject()
// TODO set mimeType when upgrading androidx lib
private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri ->
viewModel.onLogcatUriReceived(uri)
}
private val createFileLauncher =
registerForActivityResult(CreateDocument("text/plain")) { uri ->
viewModel.onLogcatUriReceived(uri)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads {

View file

@ -0,0 +1,64 @@
package com.stevesoltys.seedvault.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class SchedulingFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val viewModel: SettingsViewModel by sharedViewModel()
private val settingsManager: SettingsManager by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads {
setPreferencesFromResource(R.xml.settings_scheduling, rootKey)
PreferenceManager.setDefaultValues(requireContext(), R.xml.settings_scheduling, false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val storage = settingsManager.getStorage()
if (storage?.isUsb == true) {
findPreference<PreferenceCategory>("scheduling_category_conditions")?.isEnabled = false
}
}
override fun onStart() {
super.onStart()
activity?.setTitle(R.string.settings_backup_scheduling_title)
}
override fun onResume() {
super.onResume()
settingsManager.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
settingsManager.unregisterOnSharedPreferenceChangeListener(this)
}
// we can not use setOnPreferenceChangeListener() because that gets called
// before prefs were saved
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
PREF_KEY_SCHED_FREQ -> viewModel.scheduleAppBackup(CANCEL_AND_REENQUEUE)
PREF_KEY_SCHED_METERED -> viewModel.scheduleAppBackup(UPDATE)
PREF_KEY_SCHED_CHARGING -> viewModel.scheduleAppBackup(UPDATE)
}
}
}

View file

@ -19,12 +19,15 @@ import androidx.preference.Preference
import androidx.preference.Preference.OnPreferenceChangeListener
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.WorkInfo
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.restore.RestoreActivity
import com.stevesoltys.seedvault.ui.toRelativeTime
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import java.util.concurrent.TimeUnit
private val TAG = SettingsFragment::class.java.name
@ -39,6 +42,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var apkBackup: TwoStatePreference
private lateinit var backupLocation: Preference
private lateinit var backupStatus: Preference
private lateinit var backupScheduling: Preference
private lateinit var backupStorage: TwoStatePreference
private lateinit var backupRecoveryCode: Preference
@ -121,6 +125,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
return@OnPreferenceChangeListener false
}
backupStatus = findPreference("backup_status")!!
backupScheduling = findPreference("backup_scheduling")!!
backupStorage = findPreference("backup_storage")!!
backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
@ -141,17 +146,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
super.onViewCreated(view, savedInstanceState)
viewModel.lastBackupTime.observe(viewLifecycleOwner) { time ->
setAppBackupStatusSummary(
lastBackupInMillis = time,
nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis,
)
setAppBackupStatusSummary(time)
}
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
viewModel.onWorkerStateChanged()
setAppBackupStatusSummary(
lastBackupInMillis = viewModel.lastBackupTime.value,
nextScheduleTimeMillis = workInfo?.nextScheduleTimeMillis,
)
setAppBackupSchedulingSummary(workInfo)
}
val backupFiles: Preference = findPreference("backup_files")!!
@ -170,10 +169,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
setBackupEnabledState()
setBackupLocationSummary()
setAutoRestoreState()
setAppBackupStatusSummary(
lastBackupInMillis = viewModel.lastBackupTime.value,
nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis,
)
setAppBackupStatusSummary(viewModel.lastBackupTime.value)
setAppBackupSchedulingSummary(viewModel.appBackupWorkInfo.value)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -221,7 +218,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
return try {
backupManager.isBackupEnabled = enabled
if (enabled) {
viewModel.scheduleAppBackup()
viewModel.scheduleAppBackup(CANCEL_AND_REENQUEUE)
viewModel.enableCallLogBackup()
} else {
viewModel.cancelAppBackup()
@ -265,37 +262,41 @@ class SettingsFragment : PreferenceFragmentCompat() {
backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none)
}
private fun setAppBackupStatusSummary(
lastBackupInMillis: Long?,
nextScheduleTimeMillis: Long?,
) {
val sb = StringBuilder()
private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) {
if (lastBackupInMillis != null) {
// set time of last backup
val lastBackup = lastBackupInMillis.toRelativeTime(requireContext())
sb.append(getString(R.string.settings_backup_status_summary, lastBackup))
backupStatus.summary = getString(R.string.settings_backup_status_summary, lastBackup)
}
if (nextScheduleTimeMillis != null) {
// insert linebreak, if we have text before
if (sb.isNotEmpty()) sb.append("\n")
// set time of next backup
when (nextScheduleTimeMillis) {
Long.MAX_VALUE -> {
val text = if (backupManager.isBackupEnabled && storage?.isUsb != true) {
getString(R.string.notification_title)
} else {
getString(R.string.settings_backup_last_backup_never)
}
sb.append(getString(R.string.settings_backup_status_next_backup, text))
}
}
else -> {
val text = nextScheduleTimeMillis.toRelativeTime(requireContext())
sb.append(getString(R.string.settings_backup_status_next_backup_estimate, text))
}
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
if (storage?.isUsb == true) {
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb)
return
}
if (workInfo == null) return
val nextScheduleTimeMillis = workInfo.nextScheduleTimeMillis
if (workInfo.state == WorkInfo.State.RUNNING) {
val text = getString(R.string.notification_title)
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup, text)
} else if (nextScheduleTimeMillis == Long.MAX_VALUE) {
val text = getString(R.string.settings_backup_last_backup_never)
backupScheduling.summary = getString(R.string.settings_backup_status_next_backup, text)
} else {
val diff = System.currentTimeMillis() - nextScheduleTimeMillis
val isPast = diff > TimeUnit.MINUTES.toMillis(1)
if (isPast) {
val text = getString(R.string.settings_backup_status_next_backup_past)
backupScheduling.summary =
getString(R.string.settings_backup_status_next_backup, text)
} else {
val text = nextScheduleTimeMillis.toRelativeTime(requireContext())
backupScheduling.summary =
getString(R.string.settings_backup_status_next_backup_estimate, text)
}
}
backupStatus.summary = sb.toString()
}
private fun onEnablingStorageBackup() {

View file

@ -1,6 +1,7 @@
package com.stevesoltys.seedvault.settings
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.hardware.usb.UsbDevice
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
@ -17,6 +18,9 @@ import java.util.concurrent.ConcurrentSkipListSet
internal const val PREF_KEY_TOKEN = "token"
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
internal const val PREF_KEY_AUTO_RESTORE = "auto_restore"
internal const val PREF_KEY_SCHED_FREQ = "scheduling_frequency"
internal const val PREF_KEY_SCHED_METERED = "scheduling_metered"
internal const val PREF_KEY_SCHED_CHARGING = "scheduling_charging"
private const val PREF_KEY_STORAGE_URI = "storageUri"
private const val PREF_KEY_STORAGE_NAME = "storageName"
@ -43,6 +47,14 @@ class SettingsManager(private val context: Context) {
@Volatile
private var token: Long? = null
fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}
fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
/**
* This gets accessed by non-UI threads when saving with [PreferenceManager]
* and when [isBackupEnabled] is called during a backup run.
@ -141,6 +153,16 @@ class SettingsManager(private val context: Context) {
return prefs.getBoolean(PREF_KEY_BACKUP_APK, true)
}
val backupFrequencyInMillis: Long
get() {
return prefs.getString(PREF_KEY_SCHED_FREQ, "86400000")?.toLongOrNull()
?: 86400000 // 24h
}
val useMeteredNetwork: Boolean
get() = prefs.getBoolean(PREF_KEY_SCHED_METERED, false)
val backupOnlyWhenCharging: Boolean
get() = prefs.getBoolean(PREF_KEY_SCHED_CHARGING, true)
fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false)

View file

@ -26,6 +26,8 @@ import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import androidx.recyclerview.widget.DiffUtil.calculateDiff
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.stevesoltys.seedvault.R
@ -36,7 +38,6 @@ import com.stevesoltys.seedvault.storage.StorageBackupJobService
import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.worker.AppBackupWorker
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
import kotlinx.coroutines.Dispatchers
@ -55,7 +56,6 @@ internal class SettingsViewModel(
app: Application,
settingsManager: SettingsManager,
keyManager: KeyManager,
private val notificationManager: BackupNotificationManager,
private val metadataManager: MetadataManager,
private val appListRetriever: AppListRetriever,
private val storageBackup: StorageBackup,
@ -126,7 +126,7 @@ internal class SettingsViewModel(
override fun onStorageLocationChanged() {
val storage = settingsManager.getStorage() ?: return
Log.i(TAG, "onStorageLocationChanged")
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb}")
if (storage.isUsb) {
// disable storage backup if new storage is on USB
cancelAppBackup()
@ -134,7 +134,7 @@ internal class SettingsViewModel(
} else {
// enable it, just in case the previous storage was on USB,
// also to update the network requirement of the new storage
scheduleAppBackup()
scheduleAppBackup(CANCEL_AND_REENQUEUE)
scheduleFilesBackup()
}
onStoragePropertiesChanged()
@ -248,9 +248,11 @@ internal class SettingsViewModel(
return keyManager.hasMainKey()
}
fun scheduleAppBackup() {
fun scheduleAppBackup(existingWorkPolicy: ExistingPeriodicWorkPolicy) {
val storage = settingsManager.getStorage() ?: error("no storage available")
if (!storage.isUsb && backupManager.isBackupEnabled) AppBackupWorker.schedule(app)
if (!storage.isUsb && backupManager.isBackupEnabled) {
AppBackupWorker.schedule(app, settingsManager, existingWorkPolicy)
}
}
fun scheduleFilesBackup() {

View file

@ -10,6 +10,7 @@ import android.os.UserHandle
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.lifecycle.viewModelScope
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageBackupJobService
@ -69,7 +70,9 @@ internal class BackupStorageViewModel(
private fun scheduleBackupWorkers() {
val storage = settingsManager.getStorage() ?: error("no storage available")
if (!storage.isUsb) {
if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app)
if (backupManager.isBackupEnabled) {
AppBackupWorker.schedule(app, settingsManager, CANCEL_AND_REENQUEUE)
}
if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob(
context = app,
jobServiceClass = StorageBackupJobService::class.java,

View file

@ -7,13 +7,13 @@ package com.stevesoltys.seedvault.worker
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.text.format.DateUtils.formatElapsedTime
import android.util.Log
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.ExistingWorkPolicy.REPLACE
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
@ -22,6 +22,7 @@ import androidx.work.OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
import org.koin.core.component.KoinComponent
@ -38,21 +39,43 @@ class AppBackupWorker(
internal const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP"
private const val TAG_RESCHEDULE = "com.stevesoltys.seedvault.TAG_RESCHEDULE"
fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
/**
* (Re-)schedules the [AppBackupWorker].
*
* @param existingWorkPolicy usually you want to use [ExistingPeriodicWorkPolicy.UPDATE]
* only if you are sure that work is still scheduled
* and you don't want to mess with the scheduling time.
* In most other cases, you want to use [ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE],
* because it ensures work gets schedules, even if it wasn't scheduled before.
* It will however reset the scheduling time.
*/
fun schedule(
context: Context,
settingsManager: SettingsManager,
existingWorkPolicy: ExistingPeriodicWorkPolicy,
) {
val logFrequency = formatElapsedTime(settingsManager.backupFrequencyInMillis / 1000)
Log.i(TAG, "Scheduling in $logFrequency...")
val constraints = Constraints.Builder().apply {
if (!settingsManager.useMeteredNetwork) {
Log.i(TAG, " only on unmetered networks")
setRequiredNetworkType(NetworkType.UNMETERED)
}
if (settingsManager.backupOnlyWhenCharging) {
Log.i(TAG, " only when the device is charging")
setRequiresCharging(true)
}
}.build()
val workRequest = PeriodicWorkRequestBuilder<AppBackupWorker>(
repeatInterval = 24,
repeatIntervalTimeUnit = TimeUnit.HOURS,
repeatInterval = settingsManager.backupFrequencyInMillis,
repeatIntervalTimeUnit = TimeUnit.MILLISECONDS,
flexTimeInterval = 2,
flexTimeIntervalUnit = TimeUnit.HOURS,
).setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
.build()
val workManager = WorkManager.getInstance(context)
Log.i(TAG, "Scheduling app backup: $workRequest")
Log.i(TAG, " workRequest: ${workRequest.id}")
workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, existingWorkPolicy, workRequest)
}
@ -74,6 +97,7 @@ class AppBackupWorker(
}
private val backupRequester: BackupRequester by inject()
private val settingsManager: SettingsManager by inject()
private val apkBackupManager: ApkBackupManager by inject()
private val nm: BackupNotificationManager by inject()
@ -95,7 +119,7 @@ class AppBackupWorker(
// when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow()
if (tags.contains(TAG_RESCHEDULE) && backupRequester.isBackupEnabled) {
// needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled
schedule(applicationContext, CANCEL_AND_REENQUEUE)
schedule(applicationContext, settingsManager, CANCEL_AND_REENQUEUE)
}
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorSecondary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15,13H16.5V15.82L18.94,17.23L18.19,18.53L15,16.69V13M19,8H5V19H9.67C9.24,18.09 9,17.07 9,16A7,7 0 0,1 16,9C17.07,9 18.09,9.24 19,9.67V8M5,21C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1H18V3H19A2,2 0 0,1 21,5V11.1C22.24,12.36 23,14.09 23,16A7,7 0 0,1 16,23C14.09,23 12.36,22.24 11.1,21H5M16,11.15A4.85,4.85 0 0,0 11.15,16C11.15,18.68 13.32,20.85 16,20.85A4.85,4.85 0 0,0 20.85,16C20.85,13.32 18.68,11.15 16,11.15Z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorSecondary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V5.33C17,4.6 16.4,4 15.67,4zM11,20v-5.5H9L13,7v5.5h2L11,20z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorSecondary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19 17H21V11H19M19 21H21V19H19M1 21H17V9H21V1" />
</vector>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2024 The Calyx Institute
SPDX-License-Identifier: Apache-2.0
-->
<resources>
<string-array name="settings_scheduling_frequency_labels">
<item>@string/settings_scheduling_frequency_12_hours</item>
<item>@string/settings_scheduling_frequency_daily</item>
<item>@string/settings_scheduling_frequency_3_days</item>
<item>@string/settings_scheduling_frequency_weekly</item>
</string-array>
<string-array name="settings_scheduling_frequency_values">
<item>43200000</item>
<item>86400000</item>
<item>259200000</item>
<item>604800000</item>
</string-array>
</resources>

View file

@ -32,6 +32,9 @@
<string name="settings_backup_status_summary">Last backup: %1$s</string>
<string name="settings_backup_status_next_backup">Next backup: %1$s</string>
<string name="settings_backup_status_next_backup_estimate">Next backup (estimate): %1$s</string>
<string name="settings_backup_status_next_backup_past">once conditions are fulfilled</string>
<string name="settings_backup_status_next_backup_usb">Backups will happen automatically when you plug in your USB drive</string>
<string name="settings_backup_scheduling_title">Backup scheduling</string>
<string name="settings_backup_exclude_apps">Exclude apps</string>
<string name="settings_backup_now">Backup now</string>
<string name="settings_category_storage">Storage backup (beta)</string>
@ -48,6 +51,15 @@
<string name="settings_backup_new_code_dialog_message">To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience.</string>
<string name="settings_backup_new_code_code_dialog_ok">New code</string>
<string name="settings_scheduling_frequency_title">Backup frequency</string>
<string name="settings_scheduling_frequency_12_hours">Every 12 hours</string>
<string name="settings_scheduling_frequency_daily">Daily</string>
<string name="settings_scheduling_frequency_3_days">Every 3 days</string>
<string name="settings_scheduling_frequency_weekly">Weekly</string>
<string name="settings_scheduling_category_conditions_title">Conditions</string>
<string name="settings_scheduling_metered_title">Back up when using mobile data</string>
<string name="settings_scheduling_charging_title">Back up only when charging</string>
<string name="settings_expert_title">Expert settings</string>
<string name="settings_expert_quota_title">Unlimited app quota</string>
<string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string>

View file

@ -46,6 +46,13 @@
app:summary="@string/settings_backup_apk_summary"
app:title="@string/settings_backup_apk_title" />
<androidx.preference.Preference
app:fragment="com.stevesoltys.seedvault.settings.SchedulingFragment"
app:icon="@drawable/ic_access_time"
app:key="backup_scheduling"
app:title="@string/settings_backup_scheduling_title"
app:summary="Next backup: Never" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_category_storage">

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:defaultValue="86400000"
android:entries="@array/settings_scheduling_frequency_labels"
android:entryValues="@array/settings_scheduling_frequency_values"
app:icon="@drawable/ic_access_time"
app:key="scheduling_frequency"
app:title="@string/settings_scheduling_frequency_title"
app:useSimpleSummaryProvider="true" />
<PreferenceCategory
app:key="scheduling_category_conditions"
app:singleLineTitle="false"
app:title="@string/settings_scheduling_category_conditions_title">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="scheduling_metered"
android:title="@string/settings_scheduling_metered_title"
app:icon="@drawable/ic_network_warning"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:id="@+id/d2d_backup_preference"
android:defaultValue="true"
android:key="scheduling_charging"
android:title="@string/settings_scheduling_charging_title"
app:icon="@drawable/ic_battery_charging_full"
app:singleLineTitle="false" />
</PreferenceCategory>
</PreferenceScreen>