Add preference for checking for app updates

and show time of next update check
This commit is contained in:
Torsten Grote
2025-10-28 10:23:32 -03:00
parent eec143c5fa
commit 7d1b194fe7
10 changed files with 164 additions and 56 deletions

View File

@@ -95,7 +95,7 @@ class App : Application(), Configuration.Provider, SingletonImageLoader.Factory
// bail out here if we are the ACRA process to not initialize anything in crash process
if (ACRA.isACRASenderServiceProcess()) return
RepoUpdateWorker.scheduleOrCancel(applicationContext)
RepoUpdateWorker.scheduleOrCancel(applicationContext, settingsManager.repoUpdates)
AppUpdateWorker.scheduleOrCancel(applicationContext, settingsManager.autoUpdateApps)
}

View File

@@ -63,28 +63,33 @@ class RepoUpdateWorker @AssistedInject constructor(
}
@JvmStatic
fun scheduleOrCancel(context: Context) {
fun scheduleOrCancel(context: Context, enabled: Boolean) {
val workManager = WorkManager.getInstance(context)
Log.i(TAG, "scheduleOrCancel: enqueueUniquePeriodicWork")
val networkType = NetworkType.UNMETERED
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(networkType)
.build()
val workRequest = PeriodicWorkRequestBuilder<RepoUpdateWorker>(
repeatInterval = 4,
repeatIntervalTimeUnit = TimeUnit.HOURS,
flexTimeInterval = 15,
flexTimeIntervalUnit = MINUTES,
)
.setConstraints(constraints)
.build()
workManager.enqueueUniquePeriodicWork(
UNIQUE_WORK_NAME_REPO_AUTO_UPDATE,
UPDATE,
workRequest,
)
if (enabled) {
Log.i(TAG, "scheduleOrCancel: enqueueUniquePeriodicWork")
val networkType = NetworkType.UNMETERED
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(networkType)
.build()
val workRequest = PeriodicWorkRequestBuilder<RepoUpdateWorker>(
repeatInterval = 4,
repeatIntervalTimeUnit = TimeUnit.HOURS,
flexTimeInterval = 15,
flexTimeIntervalUnit = MINUTES,
)
.setConstraints(constraints)
.build()
workManager.enqueueUniquePeriodicWork(
UNIQUE_WORK_NAME_REPO_AUTO_UPDATE,
UPDATE,
workRequest,
)
} else {
Log.w(TAG, "Cancelling job due to settings!")
workManager.cancelUniqueWork(UNIQUE_WORK_NAME_REPO_AUTO_UPDATE)
}
}
fun getAutoUpdateWorkInfo(context: Context): Flow<WorkInfo?> {

View File

@@ -10,6 +10,9 @@ object SettingsConstants {
const val PREF_KEY_THEME = "theme"
const val PREF_DEFAULT_THEME = "followSystem"
const val PREF_KEY_REPO_UPDATES = "repoUpdates"
const val PREF_DEFAULT_REPO_UPDATES = true
const val PREF_KEY_AUTO_UPDATES = "autoUpdates"
const val PREF_DEFAULT_AUTO_UPDATES = true

View File

@@ -8,6 +8,7 @@ import io.ktor.client.engine.ProxyBuilder
import io.ktor.client.engine.ProxyConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import me.zhanghai.compose.preference.createPreferenceFlow
@@ -17,12 +18,14 @@ import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_APP_LIST_SORT_ORDER
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_AUTO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_LAST_UPDATE_CHECK
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_REPO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_INCOMPATIBLE
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME
import org.fdroid.settings.SettingsConstants.PREF_KEY_APP_LIST_SORT_ORDER
import org.fdroid.settings.SettingsConstants.PREF_KEY_AUTO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_KEY_LAST_UPDATE_CHECK
import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY
import org.fdroid.settings.SettingsConstants.PREF_KEY_REPO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE
import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME
import org.fdroid.settings.SettingsConstants.getAppListSortOrder
@@ -46,9 +49,13 @@ class SettingsManager @Inject constructor(
*/
val prefsFlow by lazy { createPreferenceFlow(prefs) }
val theme get() = prefs.getString(PREF_KEY_THEME, PREF_DEFAULT_THEME)!!
val themeFlow = prefsFlow.map { it.get<String>(PREF_KEY_THEME) }
val themeFlow = prefsFlow.map { it.get<String>(PREF_KEY_THEME) }.distinctUntilChanged()
val repoUpdates get() = prefs.getBoolean(PREF_KEY_REPO_UPDATES, PREF_DEFAULT_REPO_UPDATES)
val repoUpdatesFlow
get() = prefsFlow.map { it.get<Boolean>(PREF_KEY_REPO_UPDATES) }.distinctUntilChanged()
val autoUpdateApps get() = prefs.getBoolean(PREF_KEY_AUTO_UPDATES, PREF_DEFAULT_AUTO_UPDATES)
val autoUpdateAppsFlow get() = prefsFlow.map { it.get<Boolean>(PREF_KEY_AUTO_UPDATES) }
val autoUpdateAppsFlow
get() = prefsFlow.map { it.get<Boolean>(PREF_KEY_AUTO_UPDATES) }.distinctUntilChanged()
var lastRepoUpdate: Long
get() = try {
prefs.getInt(PREF_KEY_LAST_UPDATE_CHECK, PREF_DEFAULT_LAST_UPDATE_CHECK)

View File

@@ -269,8 +269,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) {
entry(NavigationKey.Settings) {
val viewModel = hiltViewModel<SettingsViewModel>()
Settings(
prefsFlow = viewModel.prefsFlow,
nextAppUpdateFlow = viewModel.nextAppUpdateFlow,
model = viewModel.model,
onSaveLogcat = {
viewModel.onSaveLogcat(it)
backStack.removeLastOrNull()

View File

@@ -18,12 +18,16 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.BrightnessMedium
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.SystemSecurityUpdate
import androidx.compose.material.icons.filled.SystemSecurityUpdateWarning
import androidx.compose.material.icons.filled.Translate
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.filled.UpdateDisabled
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@@ -39,10 +43,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import me.zhanghai.compose.preference.MapPreferences
import me.zhanghai.compose.preference.Preferences
import me.zhanghai.compose.preference.ProvidePreferenceLocals
import me.zhanghai.compose.preference.listPreference
import me.zhanghai.compose.preference.preference
@@ -53,19 +55,21 @@ import org.fdroid.R
import org.fdroid.fdroid.ui.theme.FDroidContent
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_AUTO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_REPO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME
import org.fdroid.settings.SettingsConstants.PREF_KEY_AUTO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY
import org.fdroid.settings.SettingsConstants.PREF_KEY_REPO_UPDATES
import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME
import org.fdroid.ui.utils.asRelativeTimeString
import org.fdroid.utils.getLogName
import java.util.concurrent.TimeUnit
import java.lang.System.currentTimeMillis
import java.util.concurrent.TimeUnit.HOURS
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun Settings(
prefsFlow: MutableStateFlow<Preferences>,
nextAppUpdateFlow: Flow<Long>,
model: SettingsModel,
onSaveLogcat: (Uri?) -> Unit,
onBackClicked: () -> Unit,
) {
@@ -91,7 +95,7 @@ fun Settings(
}
val context = LocalContext.current
val res = LocalResources.current
ProvidePreferenceLocals(prefsFlow) {
ProvidePreferenceLocals(model.prefsFlow) {
val showProxyError = remember { mutableStateOf(false) }
val proxyState = rememberPreferenceState(PREF_KEY_PROXY, PREF_DEFAULT_PROXY)
LazyColumn(
@@ -112,7 +116,6 @@ fun Settings(
else -> error("Unknown value: $value")
}
)
}
listPreference(
key = PREF_KEY_THEME,
@@ -170,36 +173,88 @@ fun Settings(
key = "pref_category_updates",
title = { Text(stringResource(R.string.updates)) },
)
switchPreference(
key = PREF_KEY_REPO_UPDATES,
defaultValue = PREF_DEFAULT_REPO_UPDATES,
title = {
Text(stringResource(R.string.pref_repo_updates_title))
},
icon = { repoUpdatesEnabled ->
if (repoUpdatesEnabled) Icon(
imageVector = Icons.Default.SystemSecurityUpdate,
contentDescription = null,
) else Icon(
imageVector = Icons.Default.SystemSecurityUpdateWarning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
},
summary = { repoUpdatesEnabled ->
if (repoUpdatesEnabled) {
val nextUpdate =
model.nextRepoUpdateFlow.collectAsState(Long.MAX_VALUE).value
val nextUpdateStr = if (nextUpdate == Long.MAX_VALUE) {
stringResource(
R.string.auto_update_time,
stringResource(R.string.repositories_last_update_never)
)
} else if (nextUpdate - currentTimeMillis() <= 0) {
stringResource(R.string.auto_update_time_past)
} else {
stringResource(
R.string.auto_update_time,
nextUpdate.asRelativeTimeString()
)
}
val s = stringResource(R.string.pref_repo_updates_summary_enabled) +
"\n\n" + nextUpdateStr
Text(s)
} else {
Text(
text = stringResource(R.string.pref_repo_updates_summary_disabled),
color = MaterialTheme.colorScheme.error,
)
}
},
)
switchPreference(
key = PREF_KEY_AUTO_UPDATES,
defaultValue = PREF_DEFAULT_AUTO_UPDATES,
title = {
Text(stringResource(R.string.update_auto_install))
},
icon = {
icon = { autoUpdatesEnabled ->
Icon(
imageVector = Icons.Default.Update,
imageVector = if (autoUpdatesEnabled) {
Icons.Default.Update
} else {
Icons.Default.UpdateDisabled
},
contentDescription = null,
)
},
summary = {
val nextUpdate = nextAppUpdateFlow.collectAsState(Long.MAX_VALUE).value
val nextUpdateStr = if (nextUpdate == Long.MAX_VALUE) {
stringResource(
R.string.auto_update_time,
stringResource(R.string.repositories_last_update_never)
)
} else if (nextUpdate - System.currentTimeMillis() <= 0) {
stringResource(R.string.auto_update_time_past)
summary = { autoUpdatesEnabled ->
val s = if (autoUpdatesEnabled) {
val nextUpdate =
model.nextAppUpdateFlow.collectAsState(Long.MAX_VALUE).value
val nextUpdateStr = if (nextUpdate == Long.MAX_VALUE) {
stringResource(
R.string.auto_update_time,
stringResource(R.string.repositories_last_update_never)
)
} else if (nextUpdate - currentTimeMillis() <= 0) {
stringResource(R.string.auto_update_time_past)
} else {
stringResource(
R.string.auto_update_time,
nextUpdate.asRelativeTimeString()
)
}
stringResource(R.string.pref_auto_updates_summary_enabled) +
"\n\n" + nextUpdateStr
} else {
stringResource(
R.string.auto_update_time,
nextUpdate.asRelativeTimeString()
)
stringResource(R.string.pref_auto_updates_summary_disabled)
}
val s = stringResource(R.string.pref_auto_updates_summary) +
"\n\n" +
nextUpdateStr
Text(s)
},
)
@@ -230,7 +285,11 @@ fun Settings(
@Composable
fun SettingsPreview() {
FDroidContent {
val nextFLow = MutableStateFlow(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(12))
Settings(MutableStateFlow(MapPreferences()), nextFLow, {}, { })
val model = SettingsModel(
prefsFlow = MutableStateFlow(MapPreferences()),
nextRepoUpdateFlow = MutableStateFlow(Long.MAX_VALUE),
nextAppUpdateFlow = MutableStateFlow(currentTimeMillis() - HOURS.toMillis(12)),
)
Settings(model, {}, { })
}
}

View File

@@ -0,0 +1,11 @@
package org.fdroid.ui.settings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import me.zhanghai.compose.preference.Preferences
data class SettingsModel(
val prefsFlow: MutableStateFlow<Preferences>,
val nextRepoUpdateFlow: Flow<Long>,
val nextAppUpdateFlow: Flow<Long>,
)

View File

@@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mu.KotlinLogging
import org.fdroid.R
import org.fdroid.repo.RepoUpdateWorker
import org.fdroid.settings.SettingsManager
import org.fdroid.ui.utils.applyNewTheme
import org.fdroid.updates.AppUpdateWorker
@@ -32,8 +33,12 @@ class SettingsViewModel @Inject constructor(
) : AndroidViewModel(app) {
private val log = KotlinLogging.logger {}
val prefsFlow = settingsManager.prefsFlow
val nextAppUpdateFlow = updatesManager.nextAppUpdateFlow
val model = SettingsModel(
prefsFlow = settingsManager.prefsFlow,
nextRepoUpdateFlow = updatesManager.nextRepoUpdateFlow,
nextAppUpdateFlow = updatesManager.nextAppUpdateFlow,
)
init {
viewModelScope.launch {
@@ -42,6 +47,12 @@ class SettingsViewModel @Inject constructor(
if (it != null) applyNewTheme(it)
}
}
viewModelScope.launch {
// react to repo-update changes
settingsManager.repoUpdatesFlow.drop(1).collect { enable ->
if (enable != null) RepoUpdateWorker.scheduleOrCancel(application, enable)
}
}
viewModelScope.launch {
// react to auto-update changes
settingsManager.autoUpdateAppsFlow.drop(1).collect { enable ->

View File

@@ -19,6 +19,7 @@ import org.fdroid.database.FDroidDatabase
import org.fdroid.download.getImageModel
import org.fdroid.index.RepoManager
import org.fdroid.install.AppInstallManager
import org.fdroid.repo.RepoUpdateWorker
import org.fdroid.settings.SettingsManager
import org.fdroid.ui.apps.AppUpdateItem
import org.fdroid.utils.IoDispatcher
@@ -43,6 +44,14 @@ class UpdatesManager @Inject constructor(
private val _numUpdates = MutableStateFlow(0)
val numUpdates = _numUpdates.asStateFlow()
/**
* The time in milliseconds of the (earliest!) next repository update run.
* This is [Long.MAX_VALUE], if no time is known.
*/
val nextRepoUpdateFlow = RepoUpdateWorker.getAutoUpdateWorkInfo(context).map { workInfo ->
workInfo?.nextScheduleTimeMillis ?: Long.MAX_VALUE
}
/**
* The time in milliseconds of the (earliest!) next automatic app update run.
* This is [Long.MAX_VALUE], if no time is known.

View File

@@ -102,7 +102,11 @@
<string name="pref_language_summary">Opens system language settings</string>
<string name="pref_auto_updates_summary">Download and update apps daily when the device isn\'t being used</string>
<string name="pref_auto_updates_summary_enabled">Download and update apps daily when the device isn\'t being used</string>
<string name="pref_auto_updates_summary_disabled">Auto-updates disabled • Apps will need to be updated manually</string>
<string name="pref_repo_updates_title">Check for updates</string>
<string name="pref_repo_updates_summary_enabled">Automatically ask all repositories for app updates</string>
<string name="pref_repo_updates_summary_disabled">Do NOT check for updates • Apps will become outdated</string>
<string name="pref_category_network">Network</string>
<string name="pref_proxy_title">Connect via SOCKS proxy</string>
<string name="pref_proxy_disabled">Connect to the internet without proxy</string>