From 7d1b194fe709f1f9658b48e96bc2600a6b9f892c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 28 Oct 2025 10:23:32 -0300 Subject: [PATCH] Add preference for checking for app updates and show time of next update check --- next/src/main/kotlin/org/fdroid/App.kt | 2 +- .../org/fdroid/repo/RepoUpdateWorker.kt | 47 ++++---- .../org/fdroid/settings/SettingsConstants.kt | 3 + .../org/fdroid/settings/SettingsManager.kt | 11 +- next/src/main/kotlin/org/fdroid/ui/Main.kt | 3 +- .../kotlin/org/fdroid/ui/settings/Settings.kt | 113 +++++++++++++----- .../org/fdroid/ui/settings/SettingsModel.kt | 11 ++ .../fdroid/ui/settings/SettingsViewModel.kt | 15 ++- .../org/fdroid/updates/UpdatesManager.kt | 9 ++ next/src/main/res/values/strings-next.xml | 6 +- 10 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 next/src/main/kotlin/org/fdroid/ui/settings/SettingsModel.kt diff --git a/next/src/main/kotlin/org/fdroid/App.kt b/next/src/main/kotlin/org/fdroid/App.kt index 4f02367cd..0e66df2f5 100644 --- a/next/src/main/kotlin/org/fdroid/App.kt +++ b/next/src/main/kotlin/org/fdroid/App.kt @@ -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) } diff --git a/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt b/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt index aa18aa70d..e18465dbe 100644 --- a/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt +++ b/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt @@ -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( - 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( + 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 { diff --git a/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt b/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt index 64987a885..15f7a21e5 100644 --- a/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt +++ b/next/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt @@ -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 diff --git a/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 15e5e8f90..6cf1f638c 100644 --- a/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -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(PREF_KEY_THEME) } + val themeFlow = prefsFlow.map { it.get(PREF_KEY_THEME) }.distinctUntilChanged() + val repoUpdates get() = prefs.getBoolean(PREF_KEY_REPO_UPDATES, PREF_DEFAULT_REPO_UPDATES) + val repoUpdatesFlow + get() = prefsFlow.map { it.get(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(PREF_KEY_AUTO_UPDATES) } + val autoUpdateAppsFlow + get() = prefsFlow.map { it.get(PREF_KEY_AUTO_UPDATES) }.distinctUntilChanged() var lastRepoUpdate: Long get() = try { prefs.getInt(PREF_KEY_LAST_UPDATE_CHECK, PREF_DEFAULT_LAST_UPDATE_CHECK) diff --git a/next/src/main/kotlin/org/fdroid/ui/Main.kt b/next/src/main/kotlin/org/fdroid/ui/Main.kt index 56770d421..b30aec20f 100644 --- a/next/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/next/src/main/kotlin/org/fdroid/ui/Main.kt @@ -269,8 +269,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) { entry(NavigationKey.Settings) { val viewModel = hiltViewModel() Settings( - prefsFlow = viewModel.prefsFlow, - nextAppUpdateFlow = viewModel.nextAppUpdateFlow, + model = viewModel.model, onSaveLogcat = { viewModel.onSaveLogcat(it) backStack.removeLastOrNull() diff --git a/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt b/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt index 97aefe508..239d67305 100644 --- a/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt +++ b/next/src/main/kotlin/org/fdroid/ui/settings/Settings.kt @@ -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, - nextAppUpdateFlow: Flow, + 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, {}, { }) } } diff --git a/next/src/main/kotlin/org/fdroid/ui/settings/SettingsModel.kt b/next/src/main/kotlin/org/fdroid/ui/settings/SettingsModel.kt new file mode 100644 index 000000000..33b8d0f8b --- /dev/null +++ b/next/src/main/kotlin/org/fdroid/ui/settings/SettingsModel.kt @@ -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, + val nextRepoUpdateFlow: Flow, + val nextAppUpdateFlow: Flow, +) diff --git a/next/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt index 1a8583257..e9572c8ef 100644 --- a/next/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt @@ -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 -> diff --git a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt index 32fe6e48e..10111dce4 100644 --- a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt +++ b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt @@ -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. diff --git a/next/src/main/res/values/strings-next.xml b/next/src/main/res/values/strings-next.xml index cc9b64aa9..5e98f9d6a 100644 --- a/next/src/main/res/values/strings-next.xml +++ b/next/src/main/res/values/strings-next.xml @@ -102,7 +102,11 @@ Opens system language settings - Download and update apps daily when the device isn\'t being used + Download and update apps daily when the device isn\'t being used + Auto-updates disabled • Apps will need to be updated manually + Check for updates + Automatically ask all repositories for app updates + Do NOT check for updates • Apps will become outdated Network Connect via SOCKS proxy Connect to the internet without proxy