From 814a29eab883f754489997bd0fb657cdd6e5059c Mon Sep 17 00:00:00 2001 From: Mimikko-zeus Date: Tue, 23 Jun 2026 11:53:02 +0800 Subject: [PATCH] fix: prime widget data after saving config --- .../java/com/sub2api/monitor/MainActivity.kt | 16 ++- .../com/sub2api/monitor/ui/ConfigViewModel.kt | 14 ++- .../monitor/ui/SaveConfigAndPrimeWidget.kt | 48 +++++++ .../monitor/widget/GlanceWidgetUpdater.kt | 13 ++ .../monitor/widget/WidgetStateRepository.kt | 7 +- .../ui/SaveConfigAndPrimeWidgetTest.kt | 117 ++++++++++++++++++ 6 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidget.kt create mode 100644 app/src/main/java/com/sub2api/monitor/widget/GlanceWidgetUpdater.kt create mode 100644 app/src/test/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidgetTest.kt diff --git a/app/src/main/java/com/sub2api/monitor/MainActivity.kt b/app/src/main/java/com/sub2api/monitor/MainActivity.kt index 32da817..c742175 100644 --- a/app/src/main/java/com/sub2api/monitor/MainActivity.kt +++ b/app/src/main/java/com/sub2api/monitor/MainActivity.kt @@ -14,6 +14,8 @@ import com.sub2api.monitor.data.MockSub2ApiRepository import com.sub2api.monitor.sync.WorkManagerRefreshScheduler import com.sub2api.monitor.ui.ConfigScreen import com.sub2api.monitor.ui.ConfigViewModel +import com.sub2api.monitor.widget.GlanceWidgetUpdater +import com.sub2api.monitor.widget.WidgetStateRepository class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -22,11 +24,15 @@ class MainActivity : ComponentActivity() { val configRepository = remember { DataStoreConfigRepository(applicationContext) } val sub2ApiRepository = remember { MockSub2ApiRepository() } val refreshScheduler = remember { WorkManagerRefreshScheduler(applicationContext) } + val widgetStateRepository = remember { WidgetStateRepository(applicationContext) } + val widgetUpdater = remember { GlanceWidgetUpdater(applicationContext) } val configViewModel: ConfigViewModel = viewModel( factory = ConfigViewModelFactory( configRepository = configRepository, sub2ApiRepository = sub2ApiRepository, refreshScheduler = refreshScheduler, + widgetStateRepository = widgetStateRepository, + widgetUpdater = widgetUpdater, ), ) Sub2ApiMonitorApp(configViewModel) @@ -52,9 +58,17 @@ private class ConfigViewModelFactory( private val configRepository: DataStoreConfigRepository, private val sub2ApiRepository: MockSub2ApiRepository, private val refreshScheduler: WorkManagerRefreshScheduler, + private val widgetStateRepository: WidgetStateRepository, + private val widgetUpdater: GlanceWidgetUpdater, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return ConfigViewModel(configRepository, sub2ApiRepository, refreshScheduler) as T + return ConfigViewModel( + configRepository, + sub2ApiRepository, + refreshScheduler, + widgetStateRepository, + widgetUpdater, + ) as T } } diff --git a/app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt b/app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt index 40c0359..3bec1d6 100644 --- a/app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt +++ b/app/src/main/java/com/sub2api/monitor/ui/ConfigViewModel.kt @@ -34,6 +34,8 @@ class ConfigViewModel( private val configRepository: ConfigRepository, private val sub2ApiRepository: Sub2ApiRepository, private val refreshScheduler: RefreshScheduler, + private val widgetSnapshotCache: WidgetSnapshotCache, + private val widgetUpdater: WidgetUpdater, ) : ViewModel() { var uiState by mutableStateOf(ConfigUiState()) private set @@ -80,9 +82,15 @@ class ConfigViewModel( viewModelScope.launch { uiState = uiState.copy(isSaving = true, statusMessage = null) val config = uiState.toAppConfig() - configRepository.save(config) - refreshScheduler.schedule(config.refreshIntervalMinutes) - uiState = uiState.copy(isSaving = false, statusMessage = "配置已保存") + val result = saveConfigAndPrimeWidget( + config = config, + configRepository = configRepository, + sub2ApiRepository = sub2ApiRepository, + refreshScheduler = refreshScheduler, + widgetSnapshotCache = widgetSnapshotCache, + widgetUpdater = widgetUpdater, + ) + uiState = uiState.copy(isSaving = false, statusMessage = result.message) } } } diff --git a/app/src/main/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidget.kt b/app/src/main/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidget.kt new file mode 100644 index 0000000..a3eaa47 --- /dev/null +++ b/app/src/main/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidget.kt @@ -0,0 +1,48 @@ +package com.sub2api.monitor.ui + +import com.sub2api.monitor.data.AppConfig +import com.sub2api.monitor.data.ConfigRepository +import com.sub2api.monitor.data.Sub2ApiRepository +import com.sub2api.monitor.domain.Sub2ApiSnapshot +import com.sub2api.monitor.sync.RefreshScheduler + +interface WidgetSnapshotCache { + suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) + suspend fun saveError(message: String) +} + +interface WidgetUpdater { + suspend fun updateAll() +} + +data class SaveConfigResult( + val message: String, +) + +suspend fun saveConfigAndPrimeWidget( + config: AppConfig, + configRepository: ConfigRepository, + sub2ApiRepository: Sub2ApiRepository, + refreshScheduler: RefreshScheduler, + widgetSnapshotCache: WidgetSnapshotCache, + widgetUpdater: WidgetUpdater, +): SaveConfigResult { + configRepository.save(config) + refreshScheduler.schedule(config.refreshIntervalMinutes) + + val message = runCatching { + sub2ApiRepository.fetchSnapshot() + }.fold( + onSuccess = { snapshot -> + widgetSnapshotCache.saveSuccess(snapshot) + "配置已保存,监控数据已刷新" + }, + onFailure = { error -> + widgetSnapshotCache.saveError(error.message ?: "首次刷新失败") + "配置已保存,首次刷新失败" + }, + ) + + widgetUpdater.updateAll() + return SaveConfigResult(message) +} diff --git a/app/src/main/java/com/sub2api/monitor/widget/GlanceWidgetUpdater.kt b/app/src/main/java/com/sub2api/monitor/widget/GlanceWidgetUpdater.kt new file mode 100644 index 0000000..df8d1db --- /dev/null +++ b/app/src/main/java/com/sub2api/monitor/widget/GlanceWidgetUpdater.kt @@ -0,0 +1,13 @@ +package com.sub2api.monitor.widget + +import android.content.Context +import androidx.glance.appwidget.updateAll +import com.sub2api.monitor.ui.WidgetUpdater + +class GlanceWidgetUpdater( + private val context: Context, +) : WidgetUpdater { + override suspend fun updateAll() { + Sub2ApiMonitorWidget().updateAll(context) + } +} diff --git a/app/src/main/java/com/sub2api/monitor/widget/WidgetStateRepository.kt b/app/src/main/java/com/sub2api/monitor/widget/WidgetStateRepository.kt index b4dddfa..88b5d06 100644 --- a/app/src/main/java/com/sub2api/monitor/widget/WidgetStateRepository.kt +++ b/app/src/main/java/com/sub2api/monitor/widget/WidgetStateRepository.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.sub2api.monitor.domain.Sub2ApiSnapshot +import com.sub2api.monitor.ui.WidgetSnapshotCache import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.serialization.decodeFromString @@ -16,7 +17,7 @@ private val Context.widgetStateDataStore by preferencesDataStore(name = "sub2api class WidgetStateRepository( private val context: Context, private val json: Json = Json { ignoreUnknownKeys = true }, -) { +) : WidgetSnapshotCache { val lastSuccessfulSnapshot: Flow = context.widgetStateDataStore.data.map { preferences -> preferences[SNAPSHOT]?.let { value -> runCatching { json.decodeFromString(value) }.getOrNull() @@ -27,14 +28,14 @@ class WidgetStateRepository( preferences[ERROR] } - suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) { + override suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) { context.widgetStateDataStore.edit { preferences -> preferences[SNAPSHOT] = json.encodeToString(snapshot) preferences.remove(ERROR) } } - suspend fun saveError(message: String) { + override suspend fun saveError(message: String) { context.widgetStateDataStore.edit { preferences -> preferences[ERROR] = message } diff --git a/app/src/test/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidgetTest.kt b/app/src/test/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidgetTest.kt new file mode 100644 index 0000000..f6cdd23 --- /dev/null +++ b/app/src/test/java/com/sub2api/monitor/ui/SaveConfigAndPrimeWidgetTest.kt @@ -0,0 +1,117 @@ +package com.sub2api.monitor.ui + +import com.sub2api.monitor.data.AppConfig +import com.sub2api.monitor.data.ConfigRepository +import com.sub2api.monitor.data.MockSub2ApiRepository +import com.sub2api.monitor.data.Sub2ApiRepository +import com.sub2api.monitor.domain.Sub2ApiSnapshot +import com.sub2api.monitor.sync.RefreshScheduler +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class SaveConfigAndPrimeWidgetTest { + @Test + fun savingConfiguredAppImmediatelyPrimesWidgetSnapshot() = runTest { + val configRepository = FakeConfigRepository() + val scheduler = FakeRefreshScheduler() + val cache = FakeWidgetSnapshotCache() + val updater = FakeWidgetUpdater() + val config = AppConfig("https://sub2api.example.com", "secret", 30) + + val result = saveConfigAndPrimeWidget( + config = config, + configRepository = configRepository, + sub2ApiRepository = MockSub2ApiRepository(), + refreshScheduler = scheduler, + widgetSnapshotCache = cache, + widgetUpdater = updater, + ) + + assertEquals(config, configRepository.savedConfig) + assertEquals(30, scheduler.scheduledInterval) + assertNotNull(cache.savedSnapshot) + assertEquals(null, cache.savedError) + assertTrue(updater.updated) + assertEquals("配置已保存,监控数据已刷新", result.message) + } + + @Test + fun failedInitialRefreshStoresErrorAndStillSavesConfiguration() = runTest { + val configRepository = FakeConfigRepository() + val scheduler = FakeRefreshScheduler() + val cache = FakeWidgetSnapshotCache() + val updater = FakeWidgetUpdater() + val config = AppConfig("https://sub2api.example.com", "secret", 30) + + val result = saveConfigAndPrimeWidget( + config = config, + configRepository = configRepository, + sub2ApiRepository = FailingSub2ApiRepository(), + refreshScheduler = scheduler, + widgetSnapshotCache = cache, + widgetUpdater = updater, + ) + + assertEquals(config, configRepository.savedConfig) + assertEquals(30, scheduler.scheduledInterval) + assertEquals(null, cache.savedSnapshot) + assertEquals("mock failure", cache.savedError) + assertTrue(updater.updated) + assertEquals("配置已保存,首次刷新失败", result.message) + } +} + +private class FakeConfigRepository : ConfigRepository { + private val state = MutableStateFlow(AppConfig()) + var savedConfig: AppConfig? = null + + override val config: Flow = state + + override suspend fun save(config: AppConfig) { + savedConfig = config + state.value = config + } +} + +private class FakeRefreshScheduler : RefreshScheduler { + var scheduledInterval: Int? = null + + override fun schedule(intervalMinutes: Int) { + scheduledInterval = intervalMinutes + } +} + +private class FakeWidgetSnapshotCache : WidgetSnapshotCache { + var savedSnapshot: Sub2ApiSnapshot? = null + var savedError: String? = null + + override suspend fun saveSuccess(snapshot: Sub2ApiSnapshot) { + savedSnapshot = snapshot + savedError = null + } + + override suspend fun saveError(message: String) { + savedError = message + } +} + +private class FakeWidgetUpdater : WidgetUpdater { + var updated = false + + override suspend fun updateAll() { + updated = true + } +} + +private class FailingSub2ApiRepository : Sub2ApiRepository { + override suspend fun fetchSnapshot(): Sub2ApiSnapshot { + error("mock failure") + } + + override suspend fun testConnection(config: AppConfig): Result = Result.success(Unit) +}