Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b8a33ec8 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

refactor(ui): decouple install status stream from caller scope

parent 473092da
Loading
Loading
Loading
Loading
+5 −7
Original line number Diff line number Diff line
@@ -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
@@ -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> {
@@ -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()
@@ -92,7 +91,7 @@ class InstallStatusStream @Inject constructor(
            }
        }.distinctUntilChanged()

        val installedPwaFlow = pollingFlow(scope, pwaPollIntervalMs) {
        val installedPwaFlow = pollingFlow(pwaPollIntervalMs) {
            withContext(Dispatchers.IO) { pwaManager.getInstalledPwaUrls() }
        }.distinctUntilChanged()

@@ -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())
        }
+1 −1
Original line number Diff line number Diff line
@@ -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
            }
        }
+11 −10
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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 {
+2 −2
Original line number Diff line number Diff line
@@ -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(),
@@ -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()