diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsFragment.kt index f59867e5d9..1f596c49d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsFragment.kt @@ -6,60 +6,136 @@ package org.thoughtcrime.securesms.components.settings.app.updates import android.os.Build +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.Rows +import org.signal.core.ui.compose.Scaffolds +import org.signal.core.ui.compose.SignalPreview import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.DSLConfiguration -import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment -import org.thoughtcrime.securesms.components.settings.DSLSettingsText -import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.ApkUpdateJob import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds /** * Settings around app updates. Only shown for builds that manage their own app updates. */ -class AppUpdatesSettingsFragment : DSLSettingsFragment(R.string.preferences_app_updates__title) { +class AppUpdatesSettingsFragment : ComposeFragment() { - override fun bindAdapter(adapter: MappingAdapter) { - adapter.submitList(getConfiguration().toMappingModelList()) + private val viewModel: AppUpdatesSettingsViewModel by viewModels() + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + AppUpdatesSettingsScreen( + state = state, + callbacks = remember { Callbacks() } + ) } - private fun getConfiguration(): DSLConfiguration { - return configure { + override fun onResume() { + super.onResume() + viewModel.refresh() + } + + private inner class Callbacks : AppUpdatesSettingsCallbacks { + override fun onNavigationClick() { + requireActivity().onBackPressedDispatcher.onBackPressed() + } + + override fun onAutoUpdateChanged(enabled: Boolean) { + SignalStore.apkUpdate.autoUpdate = enabled + viewModel.refresh() + } + + override fun onCheckForUpdatesClick() { + AppDependencies.jobManager.add(ApkUpdateJob()) + } + } +} + +private interface AppUpdatesSettingsCallbacks { + fun onNavigationClick() = Unit + fun onAutoUpdateChanged(enabled: Boolean) = Unit + fun onCheckForUpdatesClick() = Unit + + object Empty : AppUpdatesSettingsCallbacks +} + +@Composable +private fun AppUpdatesSettingsScreen( + state: AppUpdatesSettingsState, + callbacks: AppUpdatesSettingsCallbacks +) { + Scaffolds.Settings( + title = stringResource(R.string.preferences_app_updates__title), + onNavigationClick = callbacks::onNavigationClick, + navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24) + ) { paddingValues -> + + LazyColumn( + modifier = Modifier.padding(paddingValues) + ) { if (Build.VERSION.SDK_INT >= 31) { - switchPref( - title = DSLSettingsText.from("Automatic updates"), - summary = DSLSettingsText.from("Automatically download and install app updates"), - isChecked = SignalStore.apkUpdate.autoUpdate, - onClick = { - SignalStore.apkUpdate.autoUpdate = !SignalStore.apkUpdate.autoUpdate - } + item { + Rows.ToggleRow( + checked = state.autoUpdateEnabled, + text = "Automatic updates", + label = "Automatically download and install app updates", + onCheckChanged = callbacks::onAutoUpdateChanged + ) + } + } + + item { + Rows.TextRow( + text = "Check for updates", + label = "Last checked on: ${rememberLastSuccessfulUpdateString(state.lastCheckedTime)}", + onClick = callbacks::onCheckForUpdatesClick ) } - - clickPref( - title = DSLSettingsText.from("Check for updates"), - summary = DSLSettingsText.from("Last checked on: $lastSuccessfulUpdateString"), - onClick = { - AppDependencies.jobManager.add(ApkUpdateJob()) - } - ) } } - - private val lastSuccessfulUpdateString: String - get() { - val lastUpdateTime = SignalStore.apkUpdate.lastSuccessfulCheck - - return if (lastUpdateTime > 0) { - val dateFormat = SimpleDateFormat("MMMM dd, yyyy 'at' h:mma", Locale.US) - dateFormat.format(Date(lastUpdateTime)) - } else { - "Never" - } - } +} + +@Composable +private fun rememberLastSuccessfulUpdateString(lastUpdateTime: Duration): String { + return remember(lastUpdateTime) { + if (lastUpdateTime > Duration.ZERO) { + val dateFormat = SimpleDateFormat("MMMM dd, yyyy 'at' h:mma", Locale.US) + dateFormat.format(Date(lastUpdateTime.inWholeMilliseconds)) + } else { + "Never" + } + } +} + +@SignalPreview +@Composable +private fun AppUpdatesSettingsScreenPreview() { + Previews.Preview { + AppUpdatesSettingsScreen( + state = AppUpdatesSettingsState( + lastCheckedTime = System.currentTimeMillis().milliseconds, + autoUpdateEnabled = true + ), + callbacks = AppUpdatesSettingsCallbacks.Empty + ) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsState.kt new file mode 100644 index 0000000000..5dbeff5455 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsState.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.updates + +import kotlin.time.Duration + +data class AppUpdatesSettingsState( + val lastCheckedTime: Duration, + val autoUpdateEnabled: Boolean +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsViewModel.kt new file mode 100644 index 0000000000..06a34fe75a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/updates/AppUpdatesSettingsViewModel.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.updates + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.thoughtcrime.securesms.keyvalue.SignalStore +import kotlin.time.Duration.Companion.milliseconds + +class AppUpdatesSettingsViewModel : ViewModel() { + private val internalState = MutableStateFlow(getState()) + + val state: StateFlow = internalState + + fun refresh() { + internalState.update { getState() } + } + + private fun getState(): AppUpdatesSettingsState { + return AppUpdatesSettingsState( + lastCheckedTime = SignalStore.apkUpdate.lastSuccessfulCheck.milliseconds, + autoUpdateEnabled = SignalStore.apkUpdate.autoUpdate + ) + } +}