Skip to content

Commit

Permalink
Expose scheduling options in the UI
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Feb 23, 2024
1 parent 78e48c0 commit 66a5929
Show file tree
Hide file tree
Showing 16 changed files with 267 additions and 64 deletions.
7 changes: 5 additions & 2 deletions app/src/main/java/com/stevesoltys/seedvault/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,7 +56,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()) }
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 :(
Expand All @@ -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.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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 ->
Expand All @@ -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")!!
Expand All @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -125,15 +125,15 @@ 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()
cancelFilesBackup()
} 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()
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 66a5929

Please sign in to comment.