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

Commit 675fb6fc authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor(updates): optimize CleanAPK lookup

parent 06fd16c5
Loading
Loading
Loading
Loading
Loading
+10 −8
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import foundation.e.apps.data.EnabledSourceState
import foundation.e.apps.data.EnabledStoreRepositoryProvider
import foundation.e.apps.data.application.ApplicationDataManager
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
@@ -97,16 +98,17 @@ class AppsApiImpl @Inject constructor(
    ): Pair<List<Application>, ResultStatus> {
        val status = ResultStatus.OK
        val applicationList = withContext(Dispatchers.IO) {
            val store = enabledStoreRepositoryProvider.awaitStore(Source.OPEN_SOURCE)
            if (store is CleanApkRepository) {
                store.getAppDetailsForPackages(packageNameList)
            } else {
                val list = mutableListOf<Application>()
                for (packageName in packageNameList) {
                list.add(
                    enabledStoreRepositoryProvider.awaitStore(Source.OPEN_SOURCE)
                        ?.getAppDetails(packageName)
                        ?: Application()
                )
                    list.add(store?.getAppDetails(packageName) ?: Application())
                }
                list
            }
        }

        return Pair(applicationList, status)
    }
+20 −2
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import javax.inject.Inject

class CleanApkAppsRepository @Inject constructor(
    private val cleanApkRetrofit: CleanApkRetrofit,
    private val homeConverter: HomeConverter,
    private val homeConverter: HomeConverter
) : CleanApkRepository, CleanApkDownloadInfoFetcher {
    override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
        val response = cleanApkRetrofit.getHomeScreenData(
@@ -89,8 +89,26 @@ class CleanApkAppsRepository @Inject constructor(
            architectures = SystemInfoProvider.getSupportedArchitectureList()
        )
        val app = apps.body()?.apps?.firstOrNull() ?: return Application()
        return getAppDetailsById(app._id)
    }

    override suspend fun getAppDetailsForPackages(packageNames: List<String>): List<Application> {
        return fetchAppDetailsForPackages(
            packageNames = packageNames,
            checkAvailablePackages = { packages ->
                cleanApkRetrofit.checkAvailablePackages(
                    packages = packages,
                    architectures = SystemInfoProvider.getSupportedArchitectureList()
                )
            },
            getAppDetails = ::getAppDetails,
            getAppDetailsById = ::getAppDetailsById
        )
    }

    private suspend fun getAppDetailsById(appId: String): Application {
        val response = cleanApkRetrofit.getAppOrPWADetailsByID(
            id = app._id,
            id = appId,
            architectures = SystemInfoProvider.getSupportedArchitectureList(),
            type = null
        )
+20 −2
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ import javax.inject.Inject
class CleanApkPwaRepository @Inject constructor(
    private val cleanApkRetrofit: CleanApkRetrofit,
    private val homeConverter: HomeConverter,
    @ApplicationContext val context: Context,
    @ApplicationContext val context: Context
) : CleanApkRepository {

    override suspend fun getHomeScreenData(list: MutableList<Home>): List<Home> {
@@ -84,7 +84,25 @@ class CleanApkPwaRepository @Inject constructor(
    override suspend fun getAppDetails(packageName: String): Application {
        val apps = cleanApkRetrofit.checkAvailablePackages(listOf(packageName), CleanApkRetrofit.APP_TYPE_PWA)
        val app = apps.body()?.apps?.firstOrNull() ?: return Application()
        val response = cleanApkRetrofit.getAppOrPWADetailsByID(app._id, null, null)
        return getAppDetailsById(app._id)
    }

    override suspend fun getAppDetailsForPackages(packageNames: List<String>): List<Application> {
        return fetchAppDetailsForPackages(
            packageNames = packageNames,
            checkAvailablePackages = { packages ->
                cleanApkRetrofit.checkAvailablePackages(
                    packages,
                    CleanApkRetrofit.APP_TYPE_PWA
                )
            },
            getAppDetails = ::getAppDetails,
            getAppDetailsById = ::getAppDetailsById
        )
    }

    private suspend fun getAppDetailsById(appId: String): Application {
        val response = cleanApkRetrofit.getAppOrPWADetailsByID(appId, null, null)
        return response.body()?.app?.let {
            if (it.is_pwa) {
                it.copy(
+33 −0
Original line number Diff line number Diff line
@@ -19,8 +19,12 @@
package foundation.e.apps.data.cleanapk.repositories

import foundation.e.apps.data.StoreRepository
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.data.categories.Categories
import foundation.e.apps.data.cleanapk.data.search.Search
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import retrofit2.Response

const val NUMBER_OF_ITEMS = 20
@@ -30,4 +34,33 @@ interface CleanApkRepository : StoreRepository {
    suspend fun getAppsByCategory(category: String): Response<Search>
    suspend fun getCategories(): Response<Categories>
    suspend fun checkAvailablePackages(packageNames: List<String>): Response<Search>
    suspend fun getAppDetailsForPackages(packageNames: List<String>): List<Application>
}

internal suspend fun fetchAppDetailsForPackages(
    packageNames: List<String>,
    checkAvailablePackages: suspend (List<String>) -> Response<Search>,
    getAppDetails: suspend (String) -> Application,
    getAppDetailsById: suspend (String) -> Application,
): List<Application> = coroutineScope {
    if (packageNames.isEmpty()) {
        return@coroutineScope emptyList()
    }

    val appsByPackage = checkAvailablePackages(packageNames)
        .takeIf { it.isSuccessful }
        ?.body()
        ?.takeIf { it.success }
        ?.apps
        ?.associateBy(Application::package_name)
        .orEmpty()

    packageNames.map { packageName ->
        async {
            appsByPackage[packageName]
                ?.takeIf { it._id.isNotBlank() }
                ?.let { getAppDetailsById(it._id) }
                ?: getAppDetails(packageName)
        }
    }.awaitAll()
}
+112 −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.cleanapk.repositories

import com.google.common.truth.Truth.assertThat
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.data.search.Search
import kotlinx.coroutines.test.runTest
import org.junit.Test
import retrofit2.Response

class CleanApkRepositoryBatchFetchTest {

    @Test
    fun `fetchAppDetailsForPackages returns empty for empty input`() = runTest {
        var checkCalls = 0

        val result = fetchAppDetailsForPackages(
            packageNames = emptyList(),
            checkAvailablePackages = {
                checkCalls++
                Response.success(Search(success = true))
            },
            getAppDetails = { throw AssertionError("fallback should not be called") },
            getAppDetailsById = { throw AssertionError("detail lookup should not be called") }
        )

        assertThat(result).isEmpty()
        assertThat(checkCalls).isEqualTo(0)
    }

    @Test
    fun `fetchAppDetailsForPackages uses bulk ids and falls back when needed`() = runTest {
        val fallbackPackages = mutableListOf<String>()
        val detailIds = mutableListOf<String>()

        val result = fetchAppDetailsForPackages(
            packageNames = listOf("pkg.two", "pkg.one", "pkg.missing", "pkg.blank"),
            checkAvailablePackages = {
                Response.success(
                    Search(
                        apps = listOf(
                            Application(_id = "id-one", package_name = "pkg.one"),
                            Application(_id = "id-two", package_name = "pkg.two"),
                            Application(_id = "", package_name = "pkg.blank")
                        ),
                        success = true
                    )
                )
            },
            getAppDetails = {
                fallbackPackages += it
                Application(name = "fallback-$it", package_name = it)
            },
            getAppDetailsById = {
                detailIds += it
                Application(name = "detail-$it", _id = it)
            }
        )

        assertThat(result.map(Application::name)).containsExactly(
            "detail-id-two",
            "detail-id-one",
            "fallback-pkg.missing",
            "fallback-pkg.blank"
        ).inOrder()
        assertThat(detailIds).containsExactly("id-two", "id-one")
        assertThat(fallbackPackages).containsExactly("pkg.missing", "pkg.blank")
    }

    @Test
    fun `fetchAppDetailsForPackages falls back for all packages when bulk lookup fails`() = runTest {
        val fallbackPackages = mutableListOf<String>()
        var detailLookupCalls = 0

        val result = fetchAppDetailsForPackages(
            packageNames = listOf("pkg.one", "pkg.two"),
            checkAvailablePackages = { Response.success(Search(success = false)) },
            getAppDetails = {
                fallbackPackages += it
                Application(name = "fallback-$it", package_name = it)
            },
            getAppDetailsById = {
                detailLookupCalls++
                Application(name = "detail-$it", _id = it)
            }
        )

        assertThat(result.map(Application::name)).containsExactly(
            "fallback-pkg.one",
            "fallback-pkg.two"
        ).inOrder()
        assertThat(fallbackPackages).containsExactly("pkg.one", "pkg.two")
        assertThat(detailLookupCalls).isEqualTo(0)
    }
}