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

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

refactor(sources): extract enabled source state from Stores

parent f96b608b
Loading
Loading
Loading
Loading
+153 −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

import foundation.e.apps.data.di.qualifiers.IoCoroutineScope
import foundation.e.apps.data.enums.Source
import foundation.e.apps.domain.auth.UserCapabilities
import foundation.e.apps.domain.auth.UserCapabilitiesProvider
import foundation.e.apps.domain.source.AppSource
import foundation.e.apps.domain.source.SourceSelection
import foundation.e.apps.domain.source.SourceSelectionRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class EnabledSourceState @Inject constructor(
    private val sourceSelectionRepository: SourceSelectionRepository,
    private val userCapabilitiesProvider: UserCapabilitiesProvider,
    private val storeRepositoryRegistry: StoreRepositoryRegistry,
    @IoCoroutineScope coroutineScope: CoroutineScope,
) {
    private val currentCapabilities = MutableStateFlow(
        UserCapabilities(
            canAccessPlayStore = false,
            canAccessOpenSource = false,
            canAccessPwa = false,
            canPurchaseApps = false,
        )
    )
    private val currentSourceSelection =
        MutableStateFlow(sourceSelectionRepository.currentSourceSelection())

    private val _enabledSourcesFlow = MutableStateFlow(emptySet<Source>())
    val enabledSourcesFlow: StateFlow<Set<Source>> = _enabledSourcesFlow.asStateFlow()
    private val _hasHydratedCapabilities = MutableStateFlow(false)

    init {
        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            sourceSelectionRepository.sourceSelection.collect(::updateSourceSelection)
        }

        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            updateCapabilities(userCapabilitiesProvider.resolveCapabilities())
            userCapabilitiesProvider.capabilities.collect(::updateCapabilities)
        }
    }

    val hasHydratedCapabilities: Boolean
        get() = _hasHydratedCapabilities.value

    fun getEnabledSources(): Set<Source> {
        return provideEnabledSources(
            capabilities = currentCapabilities.value,
            sourceSelection = currentSourceSelection.value,
        )
    }

    suspend fun awaitEnabledSources(): Set<Source> {
        val capabilities = userCapabilitiesProvider.resolveCapabilities()
        updateCapabilities(capabilities)
        return provideEnabledSources(
            capabilities = capabilities,
            sourceSelection = sourceSelectionRepository.currentSourceSelection(),
        )
    }

    fun getEnabledSearchSources(): List<Source> = storeRepositoryRegistry.supportedSources
        .filter { source -> source in getEnabledSources() }

    suspend fun awaitEnabledSearchSources(): List<Source> = storeRepositoryRegistry.supportedSources
        .filter { source -> source in awaitEnabledSources() }

    fun isSourceEnabled(source: Source): Boolean = source in getEnabledSources()

    suspend fun awaitIsSourceEnabled(source: Source): Boolean = source in awaitEnabledSources()

    private fun provideEnabledSources(
        capabilities: UserCapabilities,
        sourceSelection: SourceSelection,
    ): Set<Source> {
        return storeRepositoryRegistry.supportedSources
            .filter { source ->
                val appSource = source.toAppSource() ?: return@filter false
                sourceSelection.isSelected(appSource) && source.isAccessible(capabilities)
            }
            .toSet()
    }

    private fun updateCapabilities(capabilities: UserCapabilities) {
        currentCapabilities.value = capabilities
        _hasHydratedCapabilities.value = true
        _enabledSourcesFlow.value = provideEnabledSources(
            capabilities = capabilities,
            sourceSelection = currentSourceSelection.value,
        )
    }

    private fun updateSourceSelection(sourceSelection: SourceSelection) {
        currentSourceSelection.value = sourceSelection
        _enabledSourcesFlow.value = provideEnabledSources(
            capabilities = currentCapabilities.value,
            sourceSelection = sourceSelection,
        )
    }
}

internal fun EnabledSourceState.enabledStoreChanges(): Flow<Set<Source>> =
    enabledSourcesFlow
        .filter { hasHydratedCapabilities }
        .drop(1)

internal fun Source.isAccessible(capabilities: UserCapabilities): Boolean {
    return when (this) {
        Source.OPEN_SOURCE -> capabilities.canAccessOpenSource
        Source.PLAY_STORE -> capabilities.canAccessPlayStore
        Source.PWA -> capabilities.canAccessPwa
        Source.SYSTEM_APP -> false
    }
}

internal fun Source.toAppSource(): AppSource? {
    return when (this) {
        Source.OPEN_SOURCE -> AppSource.OPEN_SOURCE
        Source.PLAY_STORE -> AppSource.PLAY_STORE
        Source.PWA -> AppSource.PWA
        Source.SYSTEM_APP -> null
    }
}
+41 −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

import foundation.e.apps.data.enums.Source
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class EnabledStoreRepositoryProvider @Inject constructor(
    private val enabledSourceState: EnabledSourceState,
    private val storeRepositoryRegistry: StoreRepositoryRegistry,
) {
    suspend fun awaitEnabledStores(): Map<Source, StoreRepository> {
        return storeRepositoryRegistry.asMapFor(enabledSourceState.awaitEnabledSources())
    }

    suspend fun awaitStore(source: Source): StoreRepository? {
        return if (enabledSourceState.awaitIsSourceEnabled(source)) {
            storeRepositoryRegistry.require(source)
        } else {
            null
        }
    }
}
+60 −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

import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository
import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.playstore.PlayStoreRepository
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class StoreRepositoryRegistry @Inject constructor(
    playStoreRepository: PlayStoreRepository,
    cleanApkAppsRepository: CleanApkAppsRepository,
    cleanApkPwaRepository: CleanApkPwaRepository,
) {
    private val storeRepositories = buildStoreRepositories(
        cleanApkAppsRepository = cleanApkAppsRepository,
        cleanApkPwaRepository = cleanApkPwaRepository,
        playStoreRepository = playStoreRepository,
    )

    val supportedSources: Set<Source>
        get() = storeRepositories.keys

    fun require(source: Source): StoreRepository = storeRepositories.getValue(source)

    fun get(source: Source): StoreRepository? = storeRepositories[source]

    fun asMapFor(sources: Set<Source>): Map<Source, StoreRepository> {
        return sources.associateWith(::require)
    }
}

internal fun buildStoreRepositories(
    cleanApkAppsRepository: CleanApkAppsRepository,
    cleanApkPwaRepository: CleanApkPwaRepository,
    playStoreRepository: PlayStoreRepository,
): Map<Source, StoreRepository> = linkedMapOf(
    Source.PLAY_STORE to playStoreRepository,
    Source.OPEN_SOURCE to cleanApkAppsRepository,
    Source.PWA to cleanApkPwaRepository,
)
+20 −125
Original line number Diff line number Diff line
@@ -24,20 +24,13 @@ import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository
import foundation.e.apps.data.di.qualifiers.IoCoroutineScope
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.playstore.PlayStoreRepository
import foundation.e.apps.domain.auth.UserCapabilities
import foundation.e.apps.domain.auth.UserCapabilitiesProvider
import foundation.e.apps.domain.source.AppSource
import foundation.e.apps.domain.source.SourceSelection
import foundation.e.apps.domain.source.SourceSelectionRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton

@@ -50,42 +43,22 @@ class Stores @Inject constructor(
    private val userCapabilitiesProvider: UserCapabilitiesProvider,
    @IoCoroutineScope coroutineScope: CoroutineScope,
) {

    private val storeRepositories = buildStoreRepositories(
    private val storeRepositoryRegistry = StoreRepositoryRegistry(
        playStoreRepository = playStoreRepository,
        cleanApkAppsRepository = cleanApkAppsRepository,
        cleanApkPwaRepository = cleanApkPwaRepository,
        playStoreRepository = playStoreRepository,
    )
    private val searchEligibleSources = storeRepositories.keys.toList()
    private val currentCapabilities = MutableStateFlow(
        UserCapabilities(
            canAccessPlayStore = false,
            canAccessOpenSource = false,
            canAccessPwa = false,
            canPurchaseApps = false,
    private val enabledSourceState = EnabledSourceState(
        sourceSelectionRepository = sourceSelectionRepository,
        userCapabilitiesProvider = userCapabilitiesProvider,
        storeRepositoryRegistry = storeRepositoryRegistry,
        coroutineScope = coroutineScope,
    )
    private val enabledStoreRepositoryProvider = EnabledStoreRepositoryProvider(
        enabledSourceState = enabledSourceState,
        storeRepositoryRegistry = storeRepositoryRegistry,
    )
    private val currentSourceSelection =
        MutableStateFlow(sourceSelectionRepository.currentSourceSelection())

    private val _enabledStoresFlow = MutableStateFlow(emptySet<Source>())
    val enabledStoresFlow: StateFlow<Set<Source>> = _enabledStoresFlow.asStateFlow()
    private val _hasHydratedCapabilities = MutableStateFlow(false)

    init {
        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            sourceSelectionRepository.sourceSelection.collect { sourceSelection ->
                updateSourceSelection(sourceSelection)
            }
        }

        coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
            updateCapabilities(userCapabilitiesProvider.resolveCapabilities())
            userCapabilitiesProvider.capabilities.collect { capabilities ->
                updateCapabilities(capabilities)
            }
        }
    }
    val enabledStoresFlow: StateFlow<Set<Source>> = enabledSourceState.enabledSourcesFlow

    /**
     * Returns a synchronous snapshot of the currently enabled stores.
@@ -94,13 +67,11 @@ class Stores @Inject constructor(
     * hydrated capability state.
     */
    fun getStores(): Map<Source, StoreRepository> {
        return provideEnabledStores()
            .associateWith { storeRepositories.getValue(it) }
        return storeRepositoryRegistry.asMapFor(enabledSourceState.getEnabledSources())
    }

    suspend fun awaitEnabledStores(): Map<Source, StoreRepository> {
        return awaitEnabledStoreSources()
            .associateWith { storeRepositories.getValue(it) }
        return enabledStoreRepositoryProvider.awaitEnabledStores()
    }

    /**
@@ -108,105 +79,29 @@ class Stores @Inject constructor(
     *
     * Prefer [awaitEnabledSearchSources] when a caller must not observe pre-hydration state.
     */
    fun getEnabledSearchSources(): List<Source> =
        searchEligibleSources.filter { source -> source in provideEnabledStores() }
    fun getEnabledSearchSources(): List<Source> = enabledSourceState.getEnabledSearchSources()

    suspend fun awaitEnabledSearchSources(): List<Source> =
        searchEligibleSources.filter { source -> source in awaitEnabledStoreSources() }
    suspend fun awaitEnabledSearchSources(): List<Source> = enabledSourceState.awaitEnabledSearchSources()

    suspend fun awaitStore(source: Source): StoreRepository? = awaitEnabledStores()[source]
    suspend fun awaitStore(source: Source): StoreRepository? = enabledStoreRepositoryProvider.awaitStore(source)

    /**
     * Returns whether the given store is enabled in the current synchronous snapshot.
     *
     * Prefer [awaitIsStoreEnabled] when correctness depends on hydrated capability state.
     */
    fun isStoreEnabled(source: Source): Boolean =
        source in provideEnabledStores()
    fun isStoreEnabled(source: Source): Boolean = enabledSourceState.isSourceEnabled(source)

    suspend fun awaitIsStoreEnabled(source: Source): Boolean =
        source in awaitEnabledStoreSources()
    suspend fun awaitIsStoreEnabled(source: Source): Boolean = enabledSourceState.awaitIsSourceEnabled(source)

    /**
     * Indicates whether this instance has resolved at least one awaited capability snapshot.
     */
    val hasHydratedCapabilities: Boolean
        get() = _hasHydratedCapabilities.value

    private fun provideEnabledStores(): Set<Source> =
        provideEnabledStores(
            capabilities = currentCapabilities.value,
            sourceSelection = sourceSelectionRepository.currentSourceSelection(),
        )

    private fun provideEnabledStores(
        capabilities: UserCapabilities,
        sourceSelection: SourceSelection,
    ): Set<Source> {
        return searchEligibleSources
            .filter { source ->
                val appSource = source.toAppSource() ?: return@filter false
                sourceSelection.isSelected(appSource) && source.isAccessible(capabilities)
            }
            .toSet()
    }

    private suspend fun awaitEnabledStoreSources(): Set<Source> {
        val capabilities = userCapabilitiesProvider.resolveCapabilities()
        updateCapabilities(capabilities)
        return provideEnabledStores(
            capabilities = capabilities,
            sourceSelection = sourceSelectionRepository.currentSourceSelection(),
        )
    }

    private fun updateCapabilities(capabilities: UserCapabilities) {
        currentCapabilities.value = capabilities
        _hasHydratedCapabilities.value = true
        _enabledStoresFlow.value = provideEnabledStores(
            capabilities = capabilities,
            sourceSelection = currentSourceSelection.value,
        )
    }

    private fun updateSourceSelection(sourceSelection: SourceSelection) {
        currentSourceSelection.value = sourceSelection
        _enabledStoresFlow.value = provideEnabledStores(
            capabilities = currentCapabilities.value,
            sourceSelection = sourceSelection,
        )
    }
        get() = enabledSourceState.hasHydratedCapabilities
}

internal fun Stores.enabledStoreChanges(): Flow<Set<Source>> =
    enabledStoresFlow
        .filter { hasHydratedCapabilities }
        .drop(1)

internal fun buildStoreRepositories(
    cleanApkAppsRepository: CleanApkAppsRepository,
    cleanApkPwaRepository: CleanApkPwaRepository,
    playStoreRepository: PlayStoreRepository,
): Map<Source, StoreRepository> = linkedMapOf(
    Source.PLAY_STORE to playStoreRepository,
    Source.OPEN_SOURCE to cleanApkAppsRepository,
    Source.PWA to cleanApkPwaRepository,
)

internal fun Source.isAccessible(capabilities: UserCapabilities): Boolean {
    return when (this) {
        Source.OPEN_SOURCE -> capabilities.canAccessOpenSource
        Source.PLAY_STORE -> capabilities.canAccessPlayStore
        Source.PWA -> capabilities.canAccessPwa
        Source.SYSTEM_APP -> false
    }
}

internal fun Source.toAppSource(): AppSource? {
    return when (this) {
        Source.OPEN_SOURCE -> AppSource.OPEN_SOURCE
        Source.PLAY_STORE -> AppSource.PLAY_STORE
        Source.PWA -> AppSource.PWA
        Source.SYSTEM_APP -> null
    }
}
+6 −4
Original line number Diff line number Diff line
@@ -20,9 +20,10 @@ package foundation.e.apps.data.application

import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import foundation.e.apps.data.EnabledSourceState
import foundation.e.apps.data.EnabledStoreRepositoryProvider
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.StoreRepository
import foundation.e.apps.data.Stores
import foundation.e.apps.data.application.apps.AppsApi
import foundation.e.apps.data.application.category.CategoriesResponse
import foundation.e.apps.data.application.category.CategoryApi
@@ -46,7 +47,8 @@ class ApplicationRepository @Inject constructor(
    private val categoryApi: CategoryApi,
    private val appsApi: AppsApi,
    private val downloadInfoApi: DownloadInfoApi,
    private val stores: Stores
    private val enabledStoreRepositoryProvider: EnabledStoreRepositoryProvider,
    private val enabledSourceState: EnabledSourceState,
) {
    companion object {
        const val APP_TYPE_ANY = "any"
@@ -79,7 +81,7 @@ class ApplicationRepository @Inject constructor(

            coroutineScope {
                val resultsBySource = mutableMapOf<Source, ResultSupreme<List<Home>>>()
                val enabledStores = stores.awaitEnabledStores()
                val enabledStores = enabledStoreRepositoryProvider.awaitEnabledStores()
                if (enabledStores.isEmpty()) {
                    emit(ResultSupreme.Success(emptyList<Home>()))
                    return@coroutineScope
@@ -143,7 +145,7 @@ class ApplicationRepository @Inject constructor(
    }

    suspend fun getSelectedAppTypes(): List<String> {
        return stores.awaitEnabledSearchSources().mapNotNull { source ->
        return enabledSourceState.awaitEnabledSearchSources().mapNotNull { source ->
            when (source) {
                Source.PLAY_STORE -> APP_TYPE_ANY
                Source.OPEN_SOURCE -> APP_TYPE_OPEN
Loading