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

Verified Commit 42c5b794 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: isolate AppInstallProcessor platform dependencies

Created thin wrappers for global EventBus, storage, parental control auth checking, updates DAO, updates notifier, and network connectivity check.

These wrappers replace direct static calls without changing existing behaviour and introduce mockable seams for tests in AppInstallProcessorTest.
parent 7bb7a8cf
Loading
Loading
Loading
Loading
+66 −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.di.bindings

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import foundation.e.apps.data.install.wrapper.AppEventDispatcher
import foundation.e.apps.data.install.wrapper.DefaultAppEventDispatcher
import foundation.e.apps.data.install.wrapper.DeviceNetworkStatusChecker
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
import foundation.e.apps.data.install.wrapper.ParentalControlAuthGateway
import foundation.e.apps.data.install.wrapper.ParentalControlAuthGatewayImpl
import foundation.e.apps.data.install.wrapper.StorageSpaceChecker
import foundation.e.apps.data.install.wrapper.StorageSpaceCheckerImpl
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSender
import foundation.e.apps.data.install.wrapper.UpdatesNotificationSenderImpl
import foundation.e.apps.data.install.wrapper.UpdatesTracker
import foundation.e.apps.data.install.wrapper.UpdatesTrackerImpl
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
interface AppInstallProcessorBindingsModule {

    @Binds
    @Singleton
    fun bindAppEventDispatcher(dispatcher: DefaultAppEventDispatcher): AppEventDispatcher

    @Binds
    @Singleton
    fun bindStorageSpaceChecker(checker: StorageSpaceCheckerImpl): StorageSpaceChecker

    @Binds
    @Singleton
    fun bindParentalControlAuthGateway(gateway: ParentalControlAuthGatewayImpl): ParentalControlAuthGateway

    @Binds
    @Singleton
    fun bindUpdatesTracker(tracker: UpdatesTrackerImpl): UpdatesTracker

    @Binds
    @Singleton
    fun bindUpdatesNotificationSender(sender: UpdatesNotificationSenderImpl): UpdatesNotificationSender

    @Binds
    @Singleton
    fun bindNetworkStatusChecker(checker: DeviceNetworkStatusChecker): NetworkStatusChecker
}
+30 −23
Original line number Diff line number Diff line
@@ -25,25 +25,25 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.application.UpdatesDao
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.event.EventBus
import foundation.e.apps.data.install.AppInstallComponents
import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.download.DownloadManagerUtils
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.updates.UpdatesNotifier
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.system.ParentalControlAuthenticator
import foundation.e.apps.data.system.StorageComputer
import foundation.e.apps.data.system.isNetworkAvailable
import foundation.e.apps.data.utils.getFormattedString
import foundation.e.apps.domain.ValidateAppAgeLimitUseCase
import foundation.e.apps.domain.model.ContentRatingValidity
@@ -66,6 +66,12 @@ class AppInstallProcessor @Inject constructor(
    private val sessionRepository: SessionRepository,
    private val playStoreAuthStore: PlayStoreAuthStore,
    private val storageNotificationManager: StorageNotificationManager,
    private val appEventDispatcher: AppEventDispatcher,
    private val storageSpaceChecker: StorageSpaceChecker,
    private val parentalControlAuthGateway: ParentalControlAuthGateway,
    private val updatesTracker: UpdatesTracker,
    private val updatesNotificationSender: UpdatesNotificationSender,
    private val networkStatusChecker: NetworkStatusChecker,
) {
    @Inject
    lateinit var downloadManager: DownloadManagerUtils
@@ -137,7 +143,9 @@ class AppInstallProcessor @Inject constructor(
            if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) {
                val authData = playStoreAuthStore.awaitAuthData()
                if (!appInstall.isFree && authData?.isAnonymous == true) {
                    EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message))
                    appEventDispatcher.dispatch(
                        AppEvent.ErrorMessageEvent(R.string.paid_app_anonymous_message)
                    )
                }
            }

@@ -171,17 +179,17 @@ class AppInstallProcessor @Inject constructor(
            return false
        }

        if (!context.isNetworkAvailable()) {
        if (!networkStatusChecker.isNetworkAvailable()) {
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            EventBus.invokeEvent(AppEvent.NoInternetEvent(false))
            appEventDispatcher.dispatch(AppEvent.NoInternetEvent(false))
            return false
        }

        if (StorageComputer.spaceMissing(appInstall) > 0) {
        if (storageSpaceChecker.spaceMissing(appInstall) > 0) {
            Timber.d("Storage is not available for: ${appInstall.name} size: ${appInstall.appSize}")
            storageNotificationManager.showNotEnoughSpaceNotification(appInstall)
            appInstallComponents.appManagerWrapper.installationIssue(appInstall)
            EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage))
            appEventDispatcher.dispatch(AppEvent.ErrorMessageEvent(R.string.not_enough_storage))
            return false
        }

@@ -196,13 +204,13 @@ class AppInstallProcessor @Inject constructor(
        if (ageLimitValidationResult.isSuccess()) {
            awaitInvokeAgeLimitEvent(appInstall.name)
            if (ageLimitValidationResult.data?.requestPin == true) {
                val isAuthenticated = ParentalControlAuthenticator.awaitAuthentication()
                val isAuthenticated = parentalControlAuthGateway.awaitAuthentication()
                if (isAuthenticated) {
                    ageLimitValidationResult.setData(ContentRatingValidity(true))
                }
            }
        } else {
            EventBus.invokeEvent(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc))
            appEventDispatcher.dispatch(AppEvent.ErrorMessageDialogEvent(R.string.data_load_error_desc))
        }

        var ageIsValid = true
@@ -215,7 +223,7 @@ class AppInstallProcessor @Inject constructor(

    suspend fun awaitInvokeAgeLimitEvent(type: String) {
        val deferred = CompletableDeferred<Unit>()
        EventBus.invokeEvent(AppEvent.AgeLimitRestrictionEvent(type, deferred))
        appEventDispatcher.dispatch(AppEvent.AgeLimitRestrictionEvent(type, deferred))
        deferred.await() // await closing dialog box
    }

@@ -229,7 +237,7 @@ class AppInstallProcessor @Inject constructor(
                return false
            }
            appInstallComponents.appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall)
            EventBus.invokeEvent(AppEvent.AppPurchaseEvent(appInstall))
            appEventDispatcher.dispatch(AppEvent.AppPurchaseEvent(appInstall))
            return false
        } catch (e: GplayHttpRequestException) {
            handleUpdateDownloadError(
@@ -252,7 +260,7 @@ class AppInstallProcessor @Inject constructor(
    }

    private suspend fun handleAppRestricted(appInstall: AppInstall) {
        EventBus.invokeEvent(AppEvent.AppRestrictedOrUnavailable(appInstall))
        appEventDispatcher.dispatch(AppEvent.AppRestrictedOrUnavailable(appInstall))
        appManager.addDownload(appInstall)
        appManager.updateUnavailable(appInstall)
    }
@@ -263,7 +271,7 @@ class AppInstallProcessor @Inject constructor(
        e: Exception
    ) {
        Timber.e(e, "Updating download Urls failed for $message")
        EventBus.invokeEvent(
        appEventDispatcher.dispatch(
            AppEvent.UpdateEvent(
                ResultSupreme.WorkError(
                    ResultStatus.UNKNOWN,
@@ -363,12 +371,12 @@ class AppInstallProcessor @Inject constructor(
                    appInstallComponents.appManagerWrapper.getFusedDownloadPackageStatus(appInstall)

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

                if (isUpdateCompleted()) { // show notification for ended update
                    showNotificationOnUpdateEnded()
                    UpdatesDao.clearSuccessfullyUpdatedApps()
                    updatesTracker.clearSuccessfullyUpdatedApps()
                }
            }
        }
@@ -383,18 +391,17 @@ class AppInstallProcessor @Inject constructor(
                ).contains(it.status)
            }

        return UpdatesDao.successfulUpdatedApps.isNotEmpty() && downloadListWithoutAnyIssue.isEmpty()
        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(UpdatesDao.successfulUpdatedApps.size)
            NumberFormat.getNumberInstance(locale).format(updatesTracker.successfulUpdatedAppsCount())
                .toString()

        UpdatesNotifier.showNotification(
            context,
        updatesNotificationSender.showNotification(
            context.getString(R.string.update),
            context.getString(
                R.string.message_last_update_triggered,
+33 −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.wrapper

import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.event.EventBus
import javax.inject.Inject

interface AppEventDispatcher {
    suspend fun dispatch(event: AppEvent)
}

class DefaultAppEventDispatcher @Inject constructor() : AppEventDispatcher {
    override suspend fun dispatch(event: AppEvent) {
        EventBus.invokeEvent(event)
    }
}
+36 −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.wrapper

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.data.system.isNetworkAvailable
import javax.inject.Inject

interface NetworkStatusChecker {
    fun isNetworkAvailable(): Boolean
}

class DeviceNetworkStatusChecker @Inject constructor(
    @ApplicationContext private val context: Context
) : NetworkStatusChecker {
    override fun isNetworkAvailable(): Boolean {
        return context.isNetworkAvailable()
    }
}
+32 −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.wrapper

import foundation.e.apps.data.system.ParentalControlAuthenticator
import javax.inject.Inject

interface ParentalControlAuthGateway {
    suspend fun awaitAuthentication(): Boolean
}

class ParentalControlAuthGatewayImpl @Inject constructor() : ParentalControlAuthGateway {
    override suspend fun awaitAuthentication(): Boolean {
        return ParentalControlAuthenticator.awaitAuthentication()
    }
}
Loading