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

Commit 898970e0 authored by Jonathan Klee's avatar Jonathan Klee
Browse files

Make store registry pluggable with descriptors

parent ab9f57aa
Loading
Loading
Loading
Loading
Loading
+34 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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

/**
 * Describes a store integration so that it can be discovered and toggled dynamically.
 *
 * Adding a new store only requires contributing another [StoreDescriptor] via DI.
 */
data class StoreDescriptor(
    val source: Source,
    val repository: StoreRepository,
    val isEnabled: () -> Boolean,
    val enable: () -> Unit,
    val disable: () -> Unit,
)
+27 −41
Original line number Diff line number Diff line
@@ -19,64 +19,50 @@

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.enums.Source.OPEN_SOURCE
import foundation.e.apps.data.enums.Source.PLAY_STORE
import foundation.e.apps.data.enums.Source.PWA
import foundation.e.apps.data.playstore.PlayStoreRepository
import foundation.e.apps.data.preference.AppLoungePreference
import foundation.e.apps.data.enums.Source.SYSTEM_APP
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class Stores @Inject constructor(
    private val playStoreRepository: PlayStoreRepository,
    private val cleanApkAppsRepository: CleanApkAppsRepository,
    private val cleanApkPwaRepository: CleanApkPwaRepository,
    private val appLoungePreference: AppLoungePreference
    storeDescriptors: Set<StoreDescriptor>
) {

    private val descriptorsBySource = storeDescriptors.associateBy { it.source }

    /**
     * Retrieves a map of enabled store repositories based on user preferences.
     * Retrieves a map of enabled store repositories.
     *
     * @return A map where the keys are the selected [Source] types and
     * the values are their corresponding [StoreRepository] instances.
     */
    fun getStores(): Map<Source, StoreRepository> {
        val showOpenSourceApps = appLoungePreference.isOpenSourceSelected()
        val showPwaApps = appLoungePreference.isPWASelected()
        val showPlayStoreApps = appLoungePreference.isPlayStoreSelected()
    fun getStores(): Map<Source, StoreRepository> =
        descriptorsBySource
            .filterValues { it.isEnabled() }
            .mapValues { (_, descriptor) -> descriptor.repository }

        return buildMap {
            if (showPlayStoreApps) {
                put(PLAY_STORE, playStoreRepository)
            }
            if (showOpenSourceApps) {
                put(OPEN_SOURCE, cleanApkAppsRepository)
            }
            if (showPwaApps) {
                put(PWA, cleanApkPwaRepository)
            }
    fun getStore(source: Source): StoreRepository? =
        descriptorsBySource[source]?.takeIf { it.isEnabled() }?.repository

    fun enableStore(source: Source) {
        validateToggleable(source)
        descriptorsBySource[source]?.enable?.invoke()
            ?: error("No matching Store found for $source.")
    }

    fun disableStore(source: Source) {
        validateToggleable(source)
        descriptorsBySource[source]?.disable?.invoke()
            ?: error("No matching Store found for $source.")
    }

    fun getStore(source: Source): StoreRepository? = getStores()[source]
    fun isStoreEnabled(source: Source): Boolean =
        descriptorsBySource[source]?.isEnabled?.invoke() ?: false

    fun enableStore(source: Source) = when (source) {
        OPEN_SOURCE -> appLoungePreference.enableOpenSource()
        PWA -> appLoungePreference.enablePwa()
        PLAY_STORE -> appLoungePreference.enablePlayStore()
        else -> error("No matching Store found for $source.")
    private fun validateToggleable(source: Source) {
        if (source == SYSTEM_APP) {
            error("System apps cannot be enabled or disabled.")
        }

    fun disableStore(source: Source) = when (source) {
        OPEN_SOURCE -> appLoungePreference.disableOpenSource()
        PWA -> appLoungePreference.disablePwa()
        PLAY_STORE -> appLoungePreference.disablePlayStore()
        else -> error("No matching Store found for $source.")
    }

    fun isStoreEnabled(source: Source): Boolean = getStores().containsKey(source)
}
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import foundation.e.apps.data.StoreDescriptor
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 foundation.e.apps.data.preference.AppLoungePreference
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object StoreModule {

    @Provides
    @IntoSet
    @Singleton
    fun providePlayStoreDescriptor(
        playStoreRepository: PlayStoreRepository,
        appLoungePreference: AppLoungePreference,
    ): StoreDescriptor = StoreDescriptor(
        source = Source.PLAY_STORE,
        repository = playStoreRepository,
        isEnabled = { appLoungePreference.isPlayStoreSelected() },
        enable = { appLoungePreference.enablePlayStore() },
        disable = { appLoungePreference.disablePlayStore() },
    )

    @Provides
    @IntoSet
    @Singleton
    fun provideOpenSourceDescriptor(
        cleanApkAppsRepository: CleanApkAppsRepository,
        appLoungePreference: AppLoungePreference,
    ): StoreDescriptor = StoreDescriptor(
        source = Source.OPEN_SOURCE,
        repository = cleanApkAppsRepository,
        isEnabled = { appLoungePreference.isOpenSourceSelected() },
        enable = { appLoungePreference.enableOpenSource() },
        disable = { appLoungePreference.disableOpenSource() },
    )

    @Provides
    @IntoSet
    @Singleton
    fun providePwaDescriptor(
        cleanApkPwaRepository: CleanApkPwaRepository,
        appLoungePreference: AppLoungePreference,
    ): StoreDescriptor = StoreDescriptor(
        source = Source.PWA,
        repository = cleanApkPwaRepository,
        isEnabled = { appLoungePreference.isPWASelected() },
        enable = { appLoungePreference.enablePwa() },
        disable = { appLoungePreference.disablePwa() },
    )
}
+24 −5
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.aurora.gplayapi.data.models.Category
import foundation.e.apps.R
import foundation.e.apps.data.AppSourcesContainer
import foundation.e.apps.data.StoreDescriptor
import foundation.e.apps.data.StoreRepository
import foundation.e.apps.data.Stores
import foundation.e.apps.data.application.ApplicationDataManager
import foundation.e.apps.data.application.category.CategoryApi
@@ -99,7 +101,13 @@ class CategoryApiTest {
        val applicationDataManager =
            ApplicationDataManager(appLoungePackageManager, pwaManager, visibilityFetcher)

        fakeStores = Stores(playStoreRepository, cleanApkAppsRepository, cleanApkPWARepository, appLoungePreference)
        fakeStores = Stores(
            setOf(
                createDescriptor(Source.PLAY_STORE, playStoreRepository, enabled = true),
                createDescriptor(Source.OPEN_SOURCE, cleanApkAppsRepository, enabled = true),
                createDescriptor(Source.PWA, cleanApkPWARepository, enabled = true),
            )
        )

        val appSourcesContainer =
            AppSourcesContainer(playStoreRepository, cleanApkAppsRepository, cleanApkPWARepository)
@@ -111,6 +119,21 @@ class CategoryApiTest {
        )
    }

    private fun createDescriptor(
        source: Source,
        repository: StoreRepository,
        enabled: Boolean,
    ): StoreDescriptor {
        var currentState = enabled
        return StoreDescriptor(
            source = source,
            repository = repository,
            isEnabled = { currentState },
            enable = { currentState = true },
            disable = { currentState = false },
        )
    }

    @Test
    fun `getCategory when only pwa is selected`() = runTest {
        val categories =
@@ -126,8 +149,6 @@ class CategoryApiTest {

        Mockito.`when`(context.getString(eq(R.string.pwa))).thenReturn("PWA")

        Mockito.`when`(appLoungePreference.isPWASelected()).thenReturn(true)

        val categoryListResponse =
            categoryApi.getCategoriesList(CategoryType.APPLICATION).map { it.categories }.flatten()

@@ -146,8 +167,6 @@ class CategoryApiTest {

        Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source")

        Mockito.`when`(appLoungePreference.isOpenSourceSelected()).thenReturn(true)

        fakeStores.disableStore(Source.PWA)
        fakeStores.disableStore(Source.PLAY_STORE)