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

Verified Commit 2c1e4d90 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: extract update completion handling from AppInstallProcessor

Move update bookkeeping and final notification logic behind AppUpdateCompletionHandler so the worker flow can keep its current behavior while the update rules gain their own test coverage.
parent a4844bf4
Loading
Loading
Loading
Loading
+2 −52
Original line number Diff line number Diff line
@@ -40,11 +40,8 @@ import foundation.e.apps.data.install.wrapper.AppEventDispatcher
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway
import foundation.e.apps.data.install.wrapper.StorageSpaceChecker
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender
import foundation.e.apps.data.install.wrapper.UpdatesTracker
import foundation.e.apps.data.playstore.utils.GplayHttpRequestException
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.data.utils.getFormattedString
import foundation.e.apps.domain.ValidateAppAgeLimitUseCase
import foundation.e.apps.domain.model.ContentRatingValidity
import foundation.e.apps.domain.model.User
@@ -53,8 +50,6 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.transformWhile
import timber.log.Timber
import java.text.NumberFormat
import java.util.Date
import javax.inject.Inject

@Suppress("LongParameterList")
@@ -69,9 +64,8 @@ class AppInstallProcessor @Inject constructor(
    private val appEventDispatcher: AppEventDispatcher,
    private val storageSpaceChecker: StorageSpaceChecker,
    private val parentalControlAuthGateway: ParentalControlAuthGateway,
    private val updatesTracker: UpdatesTracker,
    private val updatesNotificationSender: UpdatesNotificationSender,
    private val networkStatusChecker: NetworkStatusChecker,
    private val appUpdateCompletionHandler: AppUpdateCompletionHandler,
) {
    @Inject
    lateinit var downloadManager: DownloadManagerUtils
@@ -83,7 +77,6 @@ class AppInstallProcessor @Inject constructor(

    companion object {
        private const val TAG = "AppInstallProcessor"
        private const val DATE_FORMAT = "dd/MM/yyyy-HH:mm"
    }

    /**
@@ -365,50 +358,7 @@ class AppInstallProcessor @Inject constructor(
    private suspend fun checkUpdateWork(
        appInstall: AppInstall?
    ) {
        if (isItUpdateWork) {
            appInstall?.let {
                val packageStatus =
                    appInstallComponents.appManagerWrapper.getFusedDownloadPackageStatus(appInstall)

                if (packageStatus == Status.INSTALLED) {
                    updatesTracker.addSuccessfullyUpdatedApp(it)
                }

                if (isUpdateCompleted()) { // show notification for ended update
                    showNotificationOnUpdateEnded()
                    updatesTracker.clearSuccessfullyUpdatedApps()
                }
            }
        }
    }

    private suspend fun isUpdateCompleted(): Boolean {
        val downloadListWithoutAnyIssue =
            appInstallComponents.appInstallRepository.getDownloadList().filter {
                !listOf(
                    Status.INSTALLATION_ISSUE,
                    Status.PURCHASE_NEEDED
                ).contains(it.status)
            }

        return updatesTracker.hasSuccessfulUpdatedApps() && downloadListWithoutAnyIssue.isEmpty()
    }

    private suspend fun showNotificationOnUpdateEnded() {
        val locale = playStoreAuthStore.awaitAuthData()?.locale ?: java.util.Locale.getDefault()
        val date = Date().getFormattedString(DATE_FORMAT, locale)
        val numberOfUpdatedApps =
            NumberFormat.getNumberInstance(locale).format(updatesTracker.successfulUpdatedAppsCount())
                .toString()

        updatesNotificationSender.showNotification(
            context.getString(R.string.update),
            context.getString(
                R.string.message_last_update_triggered,
                numberOfUpdatedApps,
                date
            )
        )
        appUpdateCompletionHandler.onInstallFinished(appInstall, isItUpdateWork)
    }

    private suspend fun startAppInstallationProcess(appInstall: AppInstall) {
+93 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.install.workmanager

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.install.AppInstallRepository
import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender
import foundation.e.apps.data.install.wrapper.UpdatesTracker
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.data.utils.getFormattedString
import java.text.NumberFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject

class AppUpdateCompletionHandler @Inject constructor(
    @ApplicationContext private val context: Context,
    private val appInstallRepository: AppInstallRepository,
    private val appManagerWrapper: AppManagerWrapper,
    private val playStoreAuthStore: PlayStoreAuthStore,
    private val updatesTracker: UpdatesTracker,
    private val updatesNotificationSender: UpdatesNotificationSender,
) {
    companion object {
        private const val DATE_FORMAT = "dd/MM/yyyy-HH:mm"
    }

    suspend fun onInstallFinished(appInstall: AppInstall?, isUpdateWork: Boolean) {
        if (!isUpdateWork) {
            return
        }

        appInstall?.let {
            val packageStatus = appManagerWrapper.getFusedDownloadPackageStatus(appInstall)

            if (packageStatus == Status.INSTALLED) {
                updatesTracker.addSuccessfullyUpdatedApp(it)
            }

            if (isUpdateCompleted()) {
                showNotificationOnUpdateEnded()
                updatesTracker.clearSuccessfullyUpdatedApps()
            }
        }
    }

    private suspend fun isUpdateCompleted(): Boolean {
        val downloadListWithoutAnyIssue = appInstallRepository.getDownloadList().filter {
            !listOf(Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED).contains(it.status)
        }

        return updatesTracker.hasSuccessfulUpdatedApps() && downloadListWithoutAnyIssue.isEmpty()
    }

    private suspend fun showNotificationOnUpdateEnded() {
        val locale = playStoreAuthStore.awaitAuthData()?.locale ?: Locale.getDefault()
        val date = Date().getFormattedString(DATE_FORMAT, locale)
        val numberOfUpdatedApps =
            NumberFormat.getNumberInstance(locale)
                .format(updatesTracker.successfulUpdatedAppsCount())
                .toString()

        updatesNotificationSender.showNotification(
            context.getString(R.string.update),
            context.getString(
                R.string.message_last_update_triggered,
                numberOfUpdatedApps,
                date
            )
        )
    }
}
+15 −7
Original line number Diff line number Diff line
@@ -36,13 +36,14 @@ import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.wrapper.AppEventDispatcher
import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler
import foundation.e.apps.data.install.workmanager.InstallWorkManager
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway
import foundation.e.apps.data.install.wrapper.StorageSpaceChecker
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender
import foundation.e.apps.data.install.wrapper.UpdatesTracker
import foundation.e.apps.data.install.workmanager.AppInstallProcessor
import foundation.e.apps.data.install.workmanager.InstallWorkManager
import foundation.e.apps.data.playstore.utils.GplayHttpRequestException
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.ValidateAppAgeLimitUseCase
@@ -114,6 +115,7 @@ class AppInstallProcessorTest {
    private lateinit var updatesTracker: UpdatesTracker
    private lateinit var updatesNotificationSender: UpdatesNotificationSender
    private lateinit var networkStatusChecker: NetworkStatusChecker
    private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler

    @Before
    fun setup() {
@@ -136,6 +138,14 @@ class AppInstallProcessorTest {
            FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository)
        val appInstallComponents =
            AppInstallComponents(appInstallRepository, fakeFusedManagerRepository)
        appUpdateCompletionHandler = AppUpdateCompletionHandler(
            context,
            appInstallRepository,
            fakeFusedManagerRepository,
            playStoreAuthStore,
            updatesTracker,
            updatesNotificationSender
        )

        appInstallProcessor = AppInstallProcessor(
            context,
@@ -148,9 +158,8 @@ class AppInstallProcessorTest {
            appEventDispatcher,
            storageSpaceChecker,
            parentalControlAuthGateway,
            updatesTracker,
            updatesNotificationSender,
            networkStatusChecker
            networkStatusChecker,
            appUpdateCompletionHandler
        )
    }

@@ -629,9 +638,8 @@ class AppInstallProcessorTest {
            appEventDispatcher,
            storageSpaceChecker,
            parentalControlAuthGateway,
            updatesTracker,
            updatesNotificationSender,
            networkStatusChecker
            networkStatusChecker,
            appUpdateCompletionHandler
        )
    }
}
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.installProcessor

import android.content.Context
import com.aurora.gplayapi.data.models.AuthData
import foundation.e.apps.R
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.install.AppInstallRepository
import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender
import foundation.e.apps.data.install.wrapper.UpdatesTracker
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.util.MainCoroutineRule
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.Locale

@OptIn(ExperimentalCoroutinesApi::class)
class AppUpdateCompletionHandlerTest {

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    private lateinit var appInstallRepository: AppInstallRepository
    private lateinit var appManagerWrapper: AppManagerWrapper
    private lateinit var playStoreAuthStore: PlayStoreAuthStore
    private lateinit var updatesTracker: UpdatesTracker
    private lateinit var updatesNotificationSender: UpdatesNotificationSender
    private lateinit var context: Context
    private lateinit var handler: AppUpdateCompletionHandler

    @Before
    fun setup() {
        context = mockk(relaxed = true)
        appInstallRepository = AppInstallRepository(FakeAppInstallDAO())
        appManagerWrapper = mockk(relaxed = true)
        playStoreAuthStore = mockk(relaxed = true)
        updatesTracker = mockk(relaxed = true)
        updatesNotificationSender = mockk(relaxed = true)
        coEvery { playStoreAuthStore.awaitAuthData() } returns null
        handler = AppUpdateCompletionHandler(
            context,
            appInstallRepository,
            appManagerWrapper,
            playStoreAuthStore,
            updatesTracker,
            updatesNotificationSender
        )
    }

    @Test
    fun onInstallFinished_doesNothingWhenNotUpdateWork() = runTest {
        handler.onInstallFinished(AppInstall(id = "123", packageName = "com.example.app"), false)

        verify(exactly = 0) { appManagerWrapper.getFusedDownloadPackageStatus(any()) }
        verify(exactly = 0) { updatesTracker.addSuccessfullyUpdatedApp(any()) }
        verify(exactly = 0) { updatesNotificationSender.showNotification(any(), any()) }
    }

    @Test
    fun onInstallFinished_tracksInstalledUpdates() = runTest {
        val appInstall = AppInstall(id = "123", packageName = "com.example.app")
        every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns false

        handler.onInstallFinished(appInstall, true)

        verify { updatesTracker.addSuccessfullyUpdatedApp(appInstall) }
        verify(exactly = 0) { updatesNotificationSender.showNotification(any(), any()) }
    }

    @Test
    fun onInstallFinished_sendsNotificationWhenUpdateBatchCompletes() = runTest {
        val appInstall = AppInstall(id = "123", packageName = "com.example.app")
        every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns true
        every { updatesTracker.successfulUpdatedAppsCount() } returns 2
        stubUpdateNotificationContext()

        handler.onInstallFinished(appInstall, true)

        verify { updatesNotificationSender.showNotification("Update", "Updated message") }
        verify { updatesTracker.clearSuccessfullyUpdatedApps() }
    }

    @Test
    fun onInstallFinished_ignoresIssueAndPurchaseNeededStatusesForCompletion() = runTest {
        val appInstall = AppInstall(id = "123", packageName = "com.example.app")
        appInstallRepository.addDownload(
            AppInstall(
                id = "issue",
                status = Status.INSTALLATION_ISSUE,
                packageName = "com.example.issue"
            )
        )
        appInstallRepository.addDownload(
            AppInstall(
                id = "purchase",
                status = Status.PURCHASE_NEEDED,
                packageName = "com.example.purchase"
            )
        )
        every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns true
        every { updatesTracker.successfulUpdatedAppsCount() } returns 1
        stubUpdateNotificationContext()

        handler.onInstallFinished(appInstall, true)

        verify { updatesNotificationSender.showNotification("Update", "Updated message") }
    }

    @Test
    fun onInstallFinished_clearsTrackedUpdatesAfterNotification() = runTest {
        val appInstall = AppInstall(id = "123", packageName = "com.example.app")
        every { appManagerWrapper.getFusedDownloadPackageStatus(appInstall) } returns Status.INSTALLED
        every { updatesTracker.hasSuccessfulUpdatedApps() } returns true
        every { updatesTracker.successfulUpdatedAppsCount() } returns 1
        stubUpdateNotificationContext()

        handler.onInstallFinished(appInstall, true)

        verify { updatesTracker.clearSuccessfullyUpdatedApps() }
    }

    private suspend fun stubUpdateNotificationContext() {
        val authData = AuthData(email = "user@example.com", isAnonymous = false).apply {
            locale = Locale.US
        }
        coEvery { playStoreAuthStore.awaitAuthData() } returns authData
        every { context.getString(R.string.update) } returns "Update"
        every {
            context.getString(
                R.string.message_last_update_triggered,
                any(),
                any()
            )
        } returns "Updated message"
    }
}