Loading app/src/main/java/foundation/e/apps/ui/compose/state/InstallStatusStream.kt +5 −7 Original line number Diff line number Diff line Loading @@ -34,8 +34,8 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.install.pkg.PwaManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine Loading Loading @@ -73,7 +73,6 @@ class InstallStatusStream @Inject constructor( * Callers should collect in a scoped coroutine (e.g., ViewModel scope). */ fun stream( scope: CoroutineScope, packagePollIntervalMs: Long = DEFAULT_PACKAGE_POLL_INTERVAL_MS, pwaPollIntervalMs: Long = DEFAULT_PWA_POLL_INTERVAL_MS, ): Flow<StatusSnapshot> { Loading @@ -83,7 +82,7 @@ class InstallStatusStream @Inject constructor( .map { it.orEmpty() } .distinctUntilChanged() val installedPackagesFlow = pollingFlow(scope, packagePollIntervalMs) { val installedPackagesFlow = pollingFlow(packagePollIntervalMs) { // Package manager queries are I/O bound; keep them off the main thread. withContext(Dispatchers.IO) { appLoungePackageManager.getAllUserApps() Loading @@ -92,7 +91,7 @@ class InstallStatusStream @Inject constructor( } }.distinctUntilChanged() val installedPwaFlow = pollingFlow(scope, pwaPollIntervalMs) { val installedPwaFlow = pollingFlow(pwaPollIntervalMs) { withContext(Dispatchers.IO) { pwaManager.getInstalledPwaUrls() } }.distinctUntilChanged() Loading @@ -110,15 +109,14 @@ class InstallStatusStream @Inject constructor( } /* * Helper to emit immediately and then on a fixed interval until the scope is cancelled. * Helper to emit immediately and then on a fixed interval until collection is cancelled. */ private fun <T> pollingFlow( scope: CoroutineScope, intervalMs: Long, block: suspend () -> T, ): Flow<T> = flow { emit(block()) while (scope.isActive) { while (currentCoroutineContext().isActive) { delay(intervalMs) emit(block()) } Loading app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +1 −1 Original line number Diff line number Diff line Loading @@ -289,7 +289,7 @@ class SearchViewModelV2 @Inject constructor( private fun observeInstallStatus() { viewModelScope.launch { installStatusStream.stream(viewModelScope).collect { snapshot -> installStatusStream.stream().collect { snapshot -> statusSnapshot.value = snapshot } } Loading app/src/test/java/foundation/e/apps/ui/compose/state/InstallStatusStreamTest.kt +11 −10 Original line number Diff line number Diff line Loading @@ -28,10 +28,11 @@ import foundation.e.apps.util.MainCoroutineRule import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading Loading @@ -62,7 +63,7 @@ class InstallStatusStreamTest { val stream = InstallStatusStream(appManagerWrapper, appLoungePackageManager, pwaManager) val snapshot = stream.stream(backgroundScope, packagePollIntervalMs = 100, pwaPollIntervalMs = 100).first() val snapshot = stream.stream(packagePollIntervalMs = 100, pwaPollIntervalMs = 100).first() assertEquals(listOf(download), snapshot.downloads) assertEquals(setOf("com.example.one"), snapshot.installedPackages) Loading @@ -84,20 +85,20 @@ class InstallStatusStreamTest { every { pwaManager.getInstalledPwaUrls() } returns setOf("https://pwa.example") val stream = InstallStatusStream(appManagerWrapper, appLoungePackageManager, pwaManager) val snapshots = mutableListOf<StatusSnapshot>() val job = backgroundScope.launch { stream.stream(backgroundScope, packagePollIntervalMs = 50, pwaPollIntervalMs = 50) val snapshots = backgroundScope.async { stream.stream(packagePollIntervalMs = 50, pwaPollIntervalMs = 50) .take(2) .toList(snapshots) .toList() } runCurrent() advanceTimeBy(50) runCurrent() advanceUntilIdle() val collectedSnapshots = snapshots.await() job.join() assertEquals(setOf("com.example.one"), snapshots[0].installedPackages) assertEquals(setOf("com.example.two"), snapshots[1].installedPackages) assertEquals(setOf("com.example.one"), collectedSnapshots[0].installedPackages) assertEquals(setOf("com.example.two"), collectedSnapshots[1].installedPackages) } private fun appInfo(packageName: String) = ApplicationInfo().apply { Loading app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt +2 −2 Original line number Diff line number Diff line Loading @@ -112,7 +112,7 @@ class SearchViewModelV2Test { every { preference.disableOpenSource() } answers { openSourceSelected = false } every { preference.enablePwa() } answers { pwaSelected = true } every { preference.disablePwa() } answers { pwaSelected = false } every { installStatusStream.stream(any(), any(), any()) } returns flowOf( every { installStatusStream.stream(any(), any()) } returns flowOf( StatusSnapshot( downloads = emptyList(), installedPackages = emptySet(), Loading Loading @@ -604,7 +604,7 @@ class SearchViewModelV2Test { @Test fun `refreshInstallStatuses is safe when snapshot or progress is null`() = runTest { every { installStatusStream.stream(any(), any(), any()) } returns emptyFlow() every { installStatusStream.stream(any(), any()) } returns emptyFlow() buildViewModel() viewModel.refreshInstallStatuses() Loading Loading
app/src/main/java/foundation/e/apps/ui/compose/state/InstallStatusStream.kt +5 −7 Original line number Diff line number Diff line Loading @@ -34,8 +34,8 @@ import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.install.pkg.PwaManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine Loading Loading @@ -73,7 +73,6 @@ class InstallStatusStream @Inject constructor( * Callers should collect in a scoped coroutine (e.g., ViewModel scope). */ fun stream( scope: CoroutineScope, packagePollIntervalMs: Long = DEFAULT_PACKAGE_POLL_INTERVAL_MS, pwaPollIntervalMs: Long = DEFAULT_PWA_POLL_INTERVAL_MS, ): Flow<StatusSnapshot> { Loading @@ -83,7 +82,7 @@ class InstallStatusStream @Inject constructor( .map { it.orEmpty() } .distinctUntilChanged() val installedPackagesFlow = pollingFlow(scope, packagePollIntervalMs) { val installedPackagesFlow = pollingFlow(packagePollIntervalMs) { // Package manager queries are I/O bound; keep them off the main thread. withContext(Dispatchers.IO) { appLoungePackageManager.getAllUserApps() Loading @@ -92,7 +91,7 @@ class InstallStatusStream @Inject constructor( } }.distinctUntilChanged() val installedPwaFlow = pollingFlow(scope, pwaPollIntervalMs) { val installedPwaFlow = pollingFlow(pwaPollIntervalMs) { withContext(Dispatchers.IO) { pwaManager.getInstalledPwaUrls() } }.distinctUntilChanged() Loading @@ -110,15 +109,14 @@ class InstallStatusStream @Inject constructor( } /* * Helper to emit immediately and then on a fixed interval until the scope is cancelled. * Helper to emit immediately and then on a fixed interval until collection is cancelled. */ private fun <T> pollingFlow( scope: CoroutineScope, intervalMs: Long, block: suspend () -> T, ): Flow<T> = flow { emit(block()) while (scope.isActive) { while (currentCoroutineContext().isActive) { delay(intervalMs) emit(block()) } Loading
app/src/main/java/foundation/e/apps/ui/search/v2/SearchViewModelV2.kt +1 −1 Original line number Diff line number Diff line Loading @@ -289,7 +289,7 @@ class SearchViewModelV2 @Inject constructor( private fun observeInstallStatus() { viewModelScope.launch { installStatusStream.stream(viewModelScope).collect { snapshot -> installStatusStream.stream().collect { snapshot -> statusSnapshot.value = snapshot } } Loading
app/src/test/java/foundation/e/apps/ui/compose/state/InstallStatusStreamTest.kt +11 −10 Original line number Diff line number Diff line Loading @@ -28,10 +28,11 @@ import foundation.e.apps.util.MainCoroutineRule import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading Loading @@ -62,7 +63,7 @@ class InstallStatusStreamTest { val stream = InstallStatusStream(appManagerWrapper, appLoungePackageManager, pwaManager) val snapshot = stream.stream(backgroundScope, packagePollIntervalMs = 100, pwaPollIntervalMs = 100).first() val snapshot = stream.stream(packagePollIntervalMs = 100, pwaPollIntervalMs = 100).first() assertEquals(listOf(download), snapshot.downloads) assertEquals(setOf("com.example.one"), snapshot.installedPackages) Loading @@ -84,20 +85,20 @@ class InstallStatusStreamTest { every { pwaManager.getInstalledPwaUrls() } returns setOf("https://pwa.example") val stream = InstallStatusStream(appManagerWrapper, appLoungePackageManager, pwaManager) val snapshots = mutableListOf<StatusSnapshot>() val job = backgroundScope.launch { stream.stream(backgroundScope, packagePollIntervalMs = 50, pwaPollIntervalMs = 50) val snapshots = backgroundScope.async { stream.stream(packagePollIntervalMs = 50, pwaPollIntervalMs = 50) .take(2) .toList(snapshots) .toList() } runCurrent() advanceTimeBy(50) runCurrent() advanceUntilIdle() val collectedSnapshots = snapshots.await() job.join() assertEquals(setOf("com.example.one"), snapshots[0].installedPackages) assertEquals(setOf("com.example.two"), snapshots[1].installedPackages) assertEquals(setOf("com.example.one"), collectedSnapshots[0].installedPackages) assertEquals(setOf("com.example.two"), collectedSnapshots[1].installedPackages) } private fun appInfo(packageName: String) = ApplicationInfo().apply { Loading
app/src/test/java/foundation/e/apps/ui/search/v2/SearchViewModelV2Test.kt +2 −2 Original line number Diff line number Diff line Loading @@ -112,7 +112,7 @@ class SearchViewModelV2Test { every { preference.disableOpenSource() } answers { openSourceSelected = false } every { preference.enablePwa() } answers { pwaSelected = true } every { preference.disablePwa() } answers { pwaSelected = false } every { installStatusStream.stream(any(), any(), any()) } returns flowOf( every { installStatusStream.stream(any(), any()) } returns flowOf( StatusSnapshot( downloads = emptyList(), installedPackages = emptySet(), Loading Loading @@ -604,7 +604,7 @@ class SearchViewModelV2Test { @Test fun `refreshInstallStatuses is safe when snapshot or progress is null`() = runTest { every { installStatusStream.stream(any(), any(), any()) } returns emptyFlow() every { installStatusStream.stream(any(), any()) } returns emptyFlow() buildViewModel() viewModel.refreshInstallStatuses() Loading