Expose scheduling options in the UI
This commit is contained in:
parent
f593b66e00
commit
4eaa806636
16 changed files with 287 additions and 65 deletions
|
@ -13,6 +13,7 @@ import android.os.UserHandle
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
||||||
import com.stevesoltys.seedvault.crypto.cryptoModule
|
import com.stevesoltys.seedvault.crypto.cryptoModule
|
||||||
import com.stevesoltys.seedvault.header.headerModule
|
import com.stevesoltys.seedvault.header.headerModule
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
|
@ -56,7 +57,7 @@ open class App : Application() {
|
||||||
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
|
||||||
factory { AppListRetriever(this@App, get(), get(), get()) }
|
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 { RecoveryCodeViewModel(this@App, get(), get(), get(), get(), get(), get()) }
|
||||||
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
|
viewModel { BackupStorageViewModel(this@App, get(), get(), get()) }
|
||||||
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
viewModel { RestoreStorageViewModel(this@App, get(), get()) }
|
||||||
|
@ -133,7 +134,9 @@ open class App : Application() {
|
||||||
if (!isFrameworkSchedulingEnabled()) return // already on own scheduling
|
if (!isFrameworkSchedulingEnabled()) return // already on own scheduling
|
||||||
|
|
||||||
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
|
backupManager.setFrameworkSchedulingEnabledForUser(UserHandle.myUserId(), false)
|
||||||
if (backupManager.isBackupEnabled) AppBackupWorker.schedule(applicationContext)
|
if (backupManager.isBackupEnabled) {
|
||||||
|
AppBackupWorker.schedule(applicationContext, settingsManager, UPDATE)
|
||||||
|
}
|
||||||
// cancel old D2D worker
|
// cancel old D2D worker
|
||||||
WorkManager.getInstance(this).cancelUniqueWork("APP_BACKUP")
|
WorkManager.getInstance(this).cancelUniqueWork("APP_BACKUP")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.ui.storage.AUTHORITY_STORAGE
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
import com.stevesoltys.seedvault.worker.AppBackupWorker
|
||||||
import org.koin.core.context.GlobalContext.get
|
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 val TAG = UsbIntentReceiver::class.java.simpleName
|
||||||
|
|
||||||
private const val HOURS_AUTO_BACKUP: Long = 24
|
|
||||||
|
|
||||||
class UsbIntentReceiver : UsbMonitor() {
|
class UsbIntentReceiver : UsbMonitor() {
|
||||||
|
|
||||||
// using KoinComponent would crash robolectric tests :(
|
// using KoinComponent would crash robolectric tests :(
|
||||||
|
@ -43,11 +41,13 @@ class UsbIntentReceiver : UsbMonitor() {
|
||||||
return if (savedFlashDrive == attachedFlashDrive) {
|
return if (savedFlashDrive == attachedFlashDrive) {
|
||||||
Log.d(TAG, "Matches stored device, checking backup time...")
|
Log.d(TAG, "Matches stored device, checking backup time...")
|
||||||
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
|
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
|
||||||
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
|
if (backupMillis >= settingsManager.backupFrequencyInMillis) {
|
||||||
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
|
Log.d(TAG, "Last backup older than it should be, requesting a backup...")
|
||||||
|
Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}")
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "We have a recent backup, not requesting a new one.")
|
Log.d(TAG, "We have a recent backup, not requesting a new one.")
|
||||||
|
Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,10 +16,10 @@ class ExpertSettingsFragment : PreferenceFragmentCompat() {
|
||||||
private val viewModel: SettingsViewModel by sharedViewModel()
|
private val viewModel: SettingsViewModel by sharedViewModel()
|
||||||
private val packageService: PackageService by inject()
|
private val packageService: PackageService by inject()
|
||||||
|
|
||||||
// TODO set mimeType when upgrading androidx lib
|
private val createFileLauncher =
|
||||||
private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri ->
|
registerForActivityResult(CreateDocument("text/plain")) { uri ->
|
||||||
viewModel.onLogcatUriReceived(uri)
|
viewModel.onLogcatUriReceived(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
permitDiskReads {
|
permitDiskReads {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,12 +19,15 @@ import androidx.preference.Preference
|
||||||
import androidx.preference.Preference.OnPreferenceChangeListener
|
import androidx.preference.Preference.OnPreferenceChangeListener
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.TwoStatePreference
|
import androidx.preference.TwoStatePreference
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.permitDiskReads
|
import com.stevesoltys.seedvault.permitDiskReads
|
||||||
import com.stevesoltys.seedvault.restore.RestoreActivity
|
import com.stevesoltys.seedvault.restore.RestoreActivity
|
||||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private val TAG = SettingsFragment::class.java.name
|
private val TAG = SettingsFragment::class.java.name
|
||||||
|
|
||||||
|
@ -39,6 +42,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
private lateinit var apkBackup: TwoStatePreference
|
private lateinit var apkBackup: TwoStatePreference
|
||||||
private lateinit var backupLocation: Preference
|
private lateinit var backupLocation: Preference
|
||||||
private lateinit var backupStatus: Preference
|
private lateinit var backupStatus: Preference
|
||||||
|
private lateinit var backupScheduling: Preference
|
||||||
private lateinit var backupStorage: TwoStatePreference
|
private lateinit var backupStorage: TwoStatePreference
|
||||||
private lateinit var backupRecoveryCode: Preference
|
private lateinit var backupRecoveryCode: Preference
|
||||||
|
|
||||||
|
@ -121,6 +125,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
return@OnPreferenceChangeListener false
|
return@OnPreferenceChangeListener false
|
||||||
}
|
}
|
||||||
backupStatus = findPreference("backup_status")!!
|
backupStatus = findPreference("backup_status")!!
|
||||||
|
backupScheduling = findPreference("backup_scheduling")!!
|
||||||
|
|
||||||
backupStorage = findPreference("backup_storage")!!
|
backupStorage = findPreference("backup_storage")!!
|
||||||
backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
backupStorage.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||||
|
@ -141,17 +146,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
viewModel.lastBackupTime.observe(viewLifecycleOwner) { time ->
|
viewModel.lastBackupTime.observe(viewLifecycleOwner) { time ->
|
||||||
setAppBackupStatusSummary(
|
setAppBackupStatusSummary(time)
|
||||||
lastBackupInMillis = time,
|
|
||||||
nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
|
viewModel.appBackupWorkInfo.observe(viewLifecycleOwner) { workInfo ->
|
||||||
viewModel.onWorkerStateChanged()
|
viewModel.onWorkerStateChanged()
|
||||||
setAppBackupStatusSummary(
|
setAppBackupSchedulingSummary(workInfo)
|
||||||
lastBackupInMillis = viewModel.lastBackupTime.value,
|
|
||||||
nextScheduleTimeMillis = workInfo?.nextScheduleTimeMillis,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupFiles: Preference = findPreference("backup_files")!!
|
val backupFiles: Preference = findPreference("backup_files")!!
|
||||||
|
@ -170,10 +169,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
setBackupEnabledState()
|
setBackupEnabledState()
|
||||||
setBackupLocationSummary()
|
setBackupLocationSummary()
|
||||||
setAutoRestoreState()
|
setAutoRestoreState()
|
||||||
setAppBackupStatusSummary(
|
setAppBackupStatusSummary(viewModel.lastBackupTime.value)
|
||||||
lastBackupInMillis = viewModel.lastBackupTime.value,
|
setAppBackupSchedulingSummary(viewModel.appBackupWorkInfo.value)
|
||||||
nextScheduleTimeMillis = viewModel.appBackupWorkInfo.value?.nextScheduleTimeMillis,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
@ -221,7 +218,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
return try {
|
return try {
|
||||||
backupManager.isBackupEnabled = enabled
|
backupManager.isBackupEnabled = enabled
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
viewModel.scheduleAppBackup()
|
viewModel.scheduleAppBackup(CANCEL_AND_REENQUEUE)
|
||||||
viewModel.enableCallLogBackup()
|
viewModel.enableCallLogBackup()
|
||||||
} else {
|
} else {
|
||||||
viewModel.cancelAppBackup()
|
viewModel.cancelAppBackup()
|
||||||
|
@ -265,37 +262,41 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none)
|
backupLocation.summary = storage?.name ?: getString(R.string.settings_backup_location_none)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAppBackupStatusSummary(
|
private fun setAppBackupStatusSummary(lastBackupInMillis: Long?) {
|
||||||
lastBackupInMillis: Long?,
|
|
||||||
nextScheduleTimeMillis: Long?,
|
|
||||||
) {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
if (lastBackupInMillis != null) {
|
if (lastBackupInMillis != null) {
|
||||||
// set time of last backup
|
// set time of last backup
|
||||||
val lastBackup = lastBackupInMillis.toRelativeTime(requireContext())
|
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 -> {
|
private fun setAppBackupSchedulingSummary(workInfo: WorkInfo?) {
|
||||||
val text = nextScheduleTimeMillis.toRelativeTime(requireContext())
|
if (storage?.isUsb == true) {
|
||||||
sb.append(getString(R.string.settings_backup_status_next_backup_estimate, text))
|
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() {
|
private fun onEnablingStorageBackup() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.stevesoltys.seedvault.settings
|
package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
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_TOKEN = "token"
|
||||||
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
|
internal const val PREF_KEY_BACKUP_APK = "backup_apk"
|
||||||
internal const val PREF_KEY_AUTO_RESTORE = "auto_restore"
|
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_URI = "storageUri"
|
||||||
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
private const val PREF_KEY_STORAGE_NAME = "storageName"
|
||||||
|
@ -43,6 +47,14 @@ class SettingsManager(private val context: Context) {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var token: Long? = null
|
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]
|
* This gets accessed by non-UI threads when saving with [PreferenceManager]
|
||||||
* and when [isBackupEnabled] is called during a backup run.
|
* 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)
|
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 isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
|
||||||
|
|
||||||
fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false)
|
fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false)
|
||||||
|
|
|
@ -26,6 +26,8 @@ import androidx.lifecycle.map
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
import androidx.recyclerview.widget.DiffUtil.calculateDiff
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.stevesoltys.seedvault.R
|
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
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
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
|
||||||
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
|
import com.stevesoltys.seedvault.worker.AppBackupWorker.Companion.UNIQUE_WORK_NAME
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -55,7 +56,6 @@ internal class SettingsViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
keyManager: KeyManager,
|
keyManager: KeyManager,
|
||||||
private val notificationManager: BackupNotificationManager,
|
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
private val appListRetriever: AppListRetriever,
|
private val appListRetriever: AppListRetriever,
|
||||||
private val storageBackup: StorageBackup,
|
private val storageBackup: StorageBackup,
|
||||||
|
@ -126,7 +126,7 @@ internal class SettingsViewModel(
|
||||||
override fun onStorageLocationChanged() {
|
override fun onStorageLocationChanged() {
|
||||||
val storage = settingsManager.getStorage() ?: return
|
val storage = settingsManager.getStorage() ?: return
|
||||||
|
|
||||||
Log.i(TAG, "onStorageLocationChanged")
|
Log.i(TAG, "onStorageLocationChanged (isUsb: ${storage.isUsb}")
|
||||||
if (storage.isUsb) {
|
if (storage.isUsb) {
|
||||||
// disable storage backup if new storage is on USB
|
// disable storage backup if new storage is on USB
|
||||||
cancelAppBackup()
|
cancelAppBackup()
|
||||||
|
@ -134,7 +134,7 @@ internal class SettingsViewModel(
|
||||||
} else {
|
} else {
|
||||||
// enable it, just in case the previous storage was on USB,
|
// enable it, just in case the previous storage was on USB,
|
||||||
// also to update the network requirement of the new storage
|
// also to update the network requirement of the new storage
|
||||||
scheduleAppBackup()
|
scheduleAppBackup(CANCEL_AND_REENQUEUE)
|
||||||
scheduleFilesBackup()
|
scheduleFilesBackup()
|
||||||
}
|
}
|
||||||
onStoragePropertiesChanged()
|
onStoragePropertiesChanged()
|
||||||
|
@ -248,9 +248,11 @@ internal class SettingsViewModel(
|
||||||
return keyManager.hasMainKey()
|
return keyManager.hasMainKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scheduleAppBackup() {
|
fun scheduleAppBackup(existingWorkPolicy: ExistingPeriodicWorkPolicy) {
|
||||||
val storage = settingsManager.getStorage() ?: error("no storage available")
|
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() {
|
fun scheduleFilesBackup() {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
import com.stevesoltys.seedvault.storage.StorageBackupJobService
|
||||||
|
@ -69,7 +70,9 @@ internal class BackupStorageViewModel(
|
||||||
private fun scheduleBackupWorkers() {
|
private fun scheduleBackupWorkers() {
|
||||||
val storage = settingsManager.getStorage() ?: error("no storage available")
|
val storage = settingsManager.getStorage() ?: error("no storage available")
|
||||||
if (!storage.isUsb) {
|
if (!storage.isUsb) {
|
||||||
if (backupManager.isBackupEnabled) AppBackupWorker.schedule(app)
|
if (backupManager.isBackupEnabled) {
|
||||||
|
AppBackupWorker.schedule(app, settingsManager, CANCEL_AND_REENQUEUE)
|
||||||
|
}
|
||||||
if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob(
|
if (settingsManager.isStorageBackupEnabled()) BackupJobService.scheduleJob(
|
||||||
context = app,
|
context = app,
|
||||||
jobServiceClass = StorageBackupJobService::class.java,
|
jobServiceClass = StorageBackupJobService::class.java,
|
||||||
|
|
|
@ -7,13 +7,13 @@ package com.stevesoltys.seedvault.worker
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
|
import android.text.format.DateUtils.formatElapsedTime
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
import androidx.work.ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
|
|
||||||
import androidx.work.ExistingWorkPolicy.REPLACE
|
import androidx.work.ExistingWorkPolicy.REPLACE
|
||||||
import androidx.work.ForegroundInfo
|
import androidx.work.ForegroundInfo
|
||||||
import androidx.work.NetworkType
|
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.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
|
import com.stevesoltys.seedvault.ui.notification.NOTIFICATION_ID_OBSERVER
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
@ -38,21 +39,43 @@ class AppBackupWorker(
|
||||||
internal const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP"
|
internal const val UNIQUE_WORK_NAME = "com.stevesoltys.seedvault.APP_BACKUP"
|
||||||
private const val TAG_RESCHEDULE = "com.stevesoltys.seedvault.TAG_RESCHEDULE"
|
private const val TAG_RESCHEDULE = "com.stevesoltys.seedvault.TAG_RESCHEDULE"
|
||||||
|
|
||||||
fun schedule(context: Context, existingWorkPolicy: ExistingPeriodicWorkPolicy = UPDATE) {
|
/**
|
||||||
val constraints = Constraints.Builder()
|
* (Re-)schedules the [AppBackupWorker].
|
||||||
.setRequiredNetworkType(NetworkType.UNMETERED)
|
*
|
||||||
.setRequiresCharging(true)
|
* @param existingWorkPolicy usually you want to use [ExistingPeriodicWorkPolicy.UPDATE]
|
||||||
.build()
|
* 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>(
|
val workRequest = PeriodicWorkRequestBuilder<AppBackupWorker>(
|
||||||
repeatInterval = 24,
|
repeatInterval = settingsManager.backupFrequencyInMillis,
|
||||||
repeatIntervalTimeUnit = TimeUnit.HOURS,
|
repeatIntervalTimeUnit = TimeUnit.MILLISECONDS,
|
||||||
flexTimeInterval = 2,
|
flexTimeInterval = 2,
|
||||||
flexTimeIntervalUnit = TimeUnit.HOURS,
|
flexTimeIntervalUnit = TimeUnit.HOURS,
|
||||||
).setConstraints(constraints)
|
).setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
val workManager = WorkManager.getInstance(context)
|
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)
|
workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, existingWorkPolicy, workRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,6 +97,7 @@ class AppBackupWorker(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val backupRequester: BackupRequester by inject()
|
private val backupRequester: BackupRequester by inject()
|
||||||
|
private val settingsManager: SettingsManager by inject()
|
||||||
private val apkBackupManager: ApkBackupManager by inject()
|
private val apkBackupManager: ApkBackupManager by inject()
|
||||||
private val nm: BackupNotificationManager 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()
|
// when scheduling a OneTimeWorkRequest with the same unique name via scheduleNow()
|
||||||
if (tags.contains(TAG_RESCHEDULE) && backupRequester.isBackupEnabled) {
|
if (tags.contains(TAG_RESCHEDULE) && backupRequester.isBackupEnabled) {
|
||||||
// needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled
|
// needs to use CANCEL_AND_REENQUEUE otherwise it doesn't get scheduled
|
||||||
schedule(applicationContext, CANCEL_AND_REENQUEUE)
|
schedule(applicationContext, settingsManager, CANCEL_AND_REENQUEUE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10
app/src/main/res/drawable/ic_access_time.xml
Normal file
10
app/src/main/res/drawable/ic_access_time.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_battery_charging_full.xml
Normal file
10
app/src/main/res/drawable/ic_battery_charging_full.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_network_warning.xml
Normal file
10
app/src/main/res/drawable/ic_network_warning.xml
Normal 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>
|
20
app/src/main/res/values/arrays.xml
Normal file
20
app/src/main/res/values/arrays.xml
Normal 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>
|
|
@ -32,6 +32,9 @@
|
||||||
<string name="settings_backup_status_summary">Last backup: %1$s</string>
|
<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">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_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_exclude_apps">Exclude apps</string>
|
||||||
<string name="settings_backup_now">Backup now</string>
|
<string name="settings_backup_now">Backup now</string>
|
||||||
<string name="settings_category_storage">Storage backup (beta)</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_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_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_title">Expert settings</string>
|
||||||
<string name="settings_expert_quota_title">Unlimited app quota</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>
|
<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>
|
||||||
|
|
|
@ -46,6 +46,13 @@
|
||||||
app:summary="@string/settings_backup_apk_summary"
|
app:summary="@string/settings_backup_apk_summary"
|
||||||
app:title="@string/settings_backup_apk_title" />
|
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>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_category_storage">
|
<PreferenceCategory android:title="@string/settings_category_storage">
|
||||||
|
|
34
app/src/main/res/xml/settings_scheduling.xml
Normal file
34
app/src/main/res/xml/settings_scheduling.xml
Normal 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>
|
Loading…
Reference in a new issue