Loading app/src/main/java/foundation/e/apps/data/EnabledSourceState.kt 0 → 100644 +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 } } app/src/main/java/foundation/e/apps/data/EnabledStoreRepositoryProvider.kt 0 → 100644 +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 } } } app/src/main/java/foundation/e/apps/data/StoreRepositoryRegistry.kt 0 → 100644 +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, ) app/src/main/java/foundation/e/apps/data/Stores.kt +20 −125 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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() } /** Loading @@ -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 } } app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +6 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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" Loading Loading @@ -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 Loading Loading @@ -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 Loading
app/src/main/java/foundation/e/apps/data/EnabledSourceState.kt 0 → 100644 +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 } }
app/src/main/java/foundation/e/apps/data/EnabledStoreRepositoryProvider.kt 0 → 100644 +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 } } }
app/src/main/java/foundation/e/apps/data/StoreRepositoryRegistry.kt 0 → 100644 +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, )
app/src/main/java/foundation/e/apps/data/Stores.kt +20 −125 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. Loading @@ -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() } /** Loading @@ -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 } }
app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +6 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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" Loading Loading @@ -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 Loading Loading @@ -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