diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index 2741f0aca..6b4dd42b5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -12,6 +12,7 @@ import android.os.StrictMode import android.os.UserHandle import android.os.UserManager import android.provider.Settings +import androidx.work.ExistingPeriodicWorkPolicy.UPDATE import com.stevesoltys.seedvault.crypto.cryptoModule import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.metadata.MetadataManager @@ -55,7 +56,7 @@ open class App : Application() { factory { 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()) } @@ -132,7 +133,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) + } } private fun isFrameworkSchedulingEnabled(): Boolean = Settings.Secure.getInt( diff --git a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt index a14cd0c65..2d85f124c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt @@ -23,12 +23,9 @@ 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 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,8 +40,8 @@ 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...") true } else { Log.d(TAG, "We have a recent backup, not requesting a new one.") diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt index c7e7d3785..269f90cbd 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/ExpertSettingsFragment.kt @@ -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 { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt new file mode 100644 index 000000000..be3796a67 --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SchedulingFragment.kt @@ -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("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) + } + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt index 0c805dd27..f1427aa85 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsFragment.kt @@ -19,12 +19,14 @@ 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 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 +41,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 +124,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 +145,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?.nextScheduleTimeMillis) } val backupFiles: Preference = findPreference("backup_files")!! @@ -170,10 +168,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?.nextScheduleTimeMillis) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -221,7 +217,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 +261,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(nextScheduleTimeMillis: Long?) { + if (storage?.isUsb == true) { + backupScheduling.summary = getString(R.string.settings_backup_status_next_backup_usb) + return + } + if (nextScheduleTimeMillis == null) return + + if (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) + } + 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() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index b7ab7c69e..682508064 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -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) diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index c3a62dd02..8ae14f0fc 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -26,6 +26,8 @@ import androidx.lifecycle.liveData import androidx.lifecycle.map 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, @@ -125,7 +125,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() @@ -133,7 +133,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() @@ -247,9 +247,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() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt index 141992ea8..7a3a06a6f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/BackupStorageViewModel.kt @@ -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, diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt index 31587fd72..eb40ec346 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/AppBackupWorker.kt @@ -13,7 +13,6 @@ 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 +21,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,14 +38,22 @@ 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() + fun schedule( + context: Context, + settingsManager: SettingsManager, + existingWorkPolicy: ExistingPeriodicWorkPolicy, + ) { + val constraints = Constraints.Builder().apply { + if (!settingsManager.useMeteredNetwork) { + setRequiredNetworkType(NetworkType.UNMETERED) + } + if (settingsManager.backupOnlyWhenCharging) { + setRequiresCharging(true) + } + }.build() val workRequest = PeriodicWorkRequestBuilder( - repeatInterval = 24, - repeatIntervalTimeUnit = TimeUnit.HOURS, + repeatInterval = settingsManager.backupFrequencyInMillis, + repeatIntervalTimeUnit = TimeUnit.MILLISECONDS, flexTimeInterval = 2, flexTimeIntervalUnit = TimeUnit.HOURS, ).setConstraints(constraints) @@ -74,6 +82,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 +104,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) } } } diff --git a/app/src/main/res/drawable/ic_access_time.xml b/app/src/main/res/drawable/ic_access_time.xml new file mode 100644 index 000000000..2b1853f25 --- /dev/null +++ b/app/src/main/res/drawable/ic_access_time.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_battery_charging_full.xml b/app/src/main/res/drawable/ic_battery_charging_full.xml new file mode 100644 index 000000000..92496d40e --- /dev/null +++ b/app/src/main/res/drawable/ic_battery_charging_full.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_network_warning.xml b/app/src/main/res/drawable/ic_network_warning.xml new file mode 100644 index 000000000..a24198096 --- /dev/null +++ b/app/src/main/res/drawable/ic_network_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 000000000..aaa635565 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,20 @@ + + + + + @string/settings_scheduling_frequency_12_hours + @string/settings_scheduling_frequency_daily + @string/settings_scheduling_frequency_3_days + @string/settings_scheduling_frequency_weekly + + + + 43200000 + 86400000 + 259200000 + 604800000 + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44f1032f0..68a01bb49 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,6 +32,9 @@ Last backup: %1$s Next backup: %1$s Next backup (estimate): %1$s + once conditions are fulfilled + Backups will happen automatically when you plug in your USB drive + Backup scheduling Exclude apps Backup now Storage backup (experimental) @@ -48,6 +51,15 @@ To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience. New code + Backup frequency + Every 12 hours + Daily + Every 3 days + Weekly + Conditions + Back up when using mobile data + Back up only when charging + Expert settings Unlimited app quota 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. diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index e9034e67a..784f04009 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -46,6 +46,13 @@ app:summary="@string/settings_backup_apk_summary" app:title="@string/settings_backup_apk_title" /> + + diff --git a/app/src/main/res/xml/settings_scheduling.xml b/app/src/main/res/xml/settings_scheduling.xml new file mode 100644 index 000000000..d5bccf1e4 --- /dev/null +++ b/app/src/main/res/xml/settings_scheduling.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + +