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

Commit bd8c6fa6 authored by Hasib Prince's avatar Hasib Prince
Browse files

Merge branch '282-multiple_app_source' into 'main'

multiple source handling for Home/category page

See merge request !180
parents 6da4e723 17914931
Loading
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -135,8 +135,7 @@ dependencies {
    // Optional -- mockito-kotlin
    testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
    testImplementation 'org.mockito:mockito-inline:2.13.0'


    testImplementation "androidx.arch.core:core-testing:2.1.0"

    // Coil and PhotoView
    implementation "io.coil-kt:coil:1.4.0"
+0 −1
Original line number Diff line number Diff line
@@ -125,7 +125,6 @@ class DownloadManager @Inject constructor(
        } catch (e: Exception) {
            Timber.e(e)
        }

    }

    private fun tickerFlow(downloadId: Long, period: Duration, initialDelay: Duration = Duration.ZERO) = flow {
+296 −195
Original line number Diff line number Diff line
@@ -144,23 +144,28 @@ class FusedAPIImpl @Inject constructor(
             * To prevent waiting so long and fail early, use withTimeout{}.
             */
            withTimeout(timeoutDurationInMillis) {
                if (applicationType != APP_TYPE_ANY) {
                    val response = if (applicationType == APP_TYPE_OPEN) {
                        cleanAPKRepository.getHomeScreenData(
                if (preferenceManagerModule.isGplaySelected()) {
                    list.addAll(fetchGPlayHome(authData))
                }

                if (preferenceManagerModule.isOpenSourceSelected()) {
                    val response = cleanAPKRepository.getHomeScreenData(
                        CleanAPKInterface.APP_TYPE_ANY,
                        CleanAPKInterface.APP_SOURCE_FOSS
                    ).body()
                    } else {
                        cleanAPKRepository.getHomeScreenData(
                    response?.home?.let {
                        list.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN))
                    }
                }

                if (preferenceManagerModule.isPWASelected()) {
                    val response = cleanAPKRepository.getHomeScreenData(
                        CleanAPKInterface.APP_TYPE_PWA,
                        CleanAPKInterface.APP_SOURCE_ANY
                    ).body()
                    }
                    response?.home?.let {
                        list.addAll(generateCleanAPKHome(it, applicationType))
                        list.addAll(generateCleanAPKHome(it, APP_TYPE_PWA))
                    }
                } else {
                    list.addAll(fetchGPlayHome(authData))
                }
            }
        } catch (e: TimeoutCancellationException) {
@@ -194,20 +199,12 @@ class FusedAPIImpl @Inject constructor(
        var apiStatus: ResultStatus = ResultStatus.OK
        var applicationCategoryType = preferredApplicationType

        if (preferredApplicationType != APP_TYPE_ANY) {
            handleCleanApkCategories(preferredApplicationType, categoriesList, type).run {
                if (this != ResultStatus.OK) {
                    apiStatus = this
                }
            }
        } else {
        handleAllSourcesCategories(categoriesList, type, authData).run {
            if (first != ResultStatus.OK) {
                apiStatus = first
                applicationCategoryType = second
            }
        }
        }
        categoriesList.sortBy { item -> item.title.lowercase() }
        return Triple(categoriesList, applicationCategoryType, apiStatus)
    }
@@ -231,33 +228,149 @@ class FusedAPIImpl @Inject constructor(
         */
        return liveData {
            val packageSpecificResults = ArrayList<FusedApp>()
            var gplayPackageResult: FusedApp? = null
            var cleanapkPackageResult: FusedApp? = null
            fetchPackageSpecificResult(authData, query, packageSpecificResults)?.let {
                if (it.data?.second == true) { // if there are no data to load
                    emit(it)
                    return@liveData
                }
            }

            val status = runCodeBlockWithTimeout({
                if (preferenceManagerModule.preferredApplicationType() == APP_TYPE_ANY) {
                    try {
                        /*
                         * Surrounding with try-catch because if query is not a package name,
                         * then GPlay throws an error.
                         */
                        getApplicationDetails(query, query, authData, Origin.GPLAY).let {
                            if (it.second == ResultStatus.OK) {
                                gplayPackageResult = it.first
            val searchResult = mutableListOf<FusedApp>()
            val cleanApkResults = mutableListOf<FusedApp>()

            if (preferenceManagerModule.isOpenSourceSelected()) {
                fetchOpenSourceSearchResult(
                    this@FusedAPIImpl,
                    cleanApkResults,
                    query,
                    searchResult,
                    packageSpecificResults
                )?.let { emit(it) }
            }

            if (preferenceManagerModule.isGplaySelected()) {
                emitSource(
                    fetchGplaySearchResults(
                        query,
                        authData,
                        searchResult,
                        packageSpecificResults
                    )
                )
            }
                    } catch (e: Exception) {
                        Timber.e(e)

            if (preferenceManagerModule.isPWASelected()) {
                fetchPWASearchResult(
                    this@FusedAPIImpl,
                    query,
                    searchResult,
                    packageSpecificResults
                )?.let { emit(it) }
            }
        }
                getCleanapkSearchResult(query).let {
                    /* Cleanapk always returns something, it is never null.
                     * If nothing is found, it returns a blank FusedApp() object.
                     * Blank result to be filtered out.
                     */
                    if (it.isSuccess() && it.data!!.package_name.isNotBlank()) {
                        cleanapkPackageResult = it.data!!
    }

    private suspend fun fetchPWASearchResult(
        fusedAPIImpl: FusedAPIImpl,
        query: String,
        searchResult: MutableList<FusedApp>,
        packageSpecificResults: ArrayList<FusedApp>
    ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? {
        val pwaApps: MutableList<FusedApp> = mutableListOf()
        val status = fusedAPIImpl.runCodeBlockWithTimeout({
            getCleanAPKSearchResults(
                query,
                CleanAPKInterface.APP_SOURCE_ANY,
                CleanAPKInterface.APP_TYPE_PWA
            ).apply {
                if (this.isNotEmpty()) {
                    pwaApps.addAll(this)
                }
            }
        })

        if (pwaApps.isNotEmpty() || status != ResultStatus.OK) {
            searchResult.addAll(pwaApps)
            return ResultSupreme.create(
                status,
                Pair(
                    filterWithKeywordSearch(
                        searchResult,
                        packageSpecificResults,
                        query
                    ),
                    false
                )
            )
        }
        return null
    }

    private fun fetchGplaySearchResults(
        query: String,
        authData: AuthData,
        searchResult: MutableList<FusedApp>,
        packageSpecificResults: ArrayList<FusedApp>
    ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> =
        getGplaySearchResults(query, authData).map {
            if (it.first.isNotEmpty()) {
                searchResult.addAll(it.first)
            }
            ResultSupreme.Success(
                Pair(
                    filterWithKeywordSearch(
                        searchResult,
                        packageSpecificResults,
                        query
                    ),
                    it.second
                )
            )
        }

    private suspend fun fetchOpenSourceSearchResult(
        fusedAPIImpl: FusedAPIImpl,
        cleanApkResults: MutableList<FusedApp>,
        query: String,
        searchResult: MutableList<FusedApp>,
        packageSpecificResults: ArrayList<FusedApp>
    ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? {
        val status = fusedAPIImpl.runCodeBlockWithTimeout({
            cleanApkResults.addAll(getCleanAPKSearchResults(query))
        })

        if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) {
            searchResult.addAll(cleanApkResults)
            return ResultSupreme.create(
                status,
                Pair(
                    filterWithKeywordSearch(
                        searchResult,
                        packageSpecificResults,
                        query
                    ),
                    preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected()
                )
            )
        }
        return null
    }

    private suspend fun fetchPackageSpecificResult(
        authData: AuthData,
        query: String,
        packageSpecificResults: MutableList<FusedApp>
    ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? {
        var gplayPackageResult: FusedApp? = null
        var cleanapkPackageResult: FusedApp? = null

        val status = runCodeBlockWithTimeout({
            if (preferenceManagerModule.isGplaySelected()) {
                gplayPackageResult = getGplayPackagResult(query, authData)
            }

            if (preferenceManagerModule.isOpenSourceSelected()) {
                cleanapkPackageResult = getCleanApkPackageResult(query)
            }
        })

@@ -275,8 +388,9 @@ class FusedAPIImpl @Inject constructor(
         * Also send true in the pair to signal more results being loaded.
         */
        if (status != ResultStatus.OK) {
                emit(ResultSupreme.create(status, Pair(packageSpecificResults, true)))
                return@liveData
            return ResultSupreme.create(status, Pair(packageSpecificResults, true))
        }
        return ResultSupreme.create(status, Pair(packageSpecificResults, false))
    }

    /*
@@ -287,81 +401,41 @@ class FusedAPIImpl @Inject constructor(
             * But for the other keyword related search results, we do not allow duplicate package names.
             * We also filter out apps which are already present in packageSpecificResults list.
             */
            fun filterWithKeywordSearch(list: List<FusedApp>): List<FusedApp> {
    private fun filterWithKeywordSearch(
        list: List<FusedApp>,
        packageSpecificResults: List<FusedApp>,
        query: String
    ): List<FusedApp> {
        val filteredResults = list.distinctBy { it.package_name }
            .filter { packageSpecificResults.isEmpty() || it.package_name != query }
        return packageSpecificResults + filteredResults
    }

            val cleanApkResults = mutableListOf<FusedApp>()
            when (preferenceManagerModule.preferredApplicationType()) {
                APP_TYPE_ANY -> {
                    val status = runCodeBlockWithTimeout({
                        cleanApkResults.addAll(getCleanAPKSearchResults(query))
                    })
                    if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) {
                        /*
                         * If cleanapk results are empty, dont emit emit data as it may
                         * briefly show "No apps found..."
                         * If status is timeout, then do emit the value.
                         * Send true in the pair to signal more results (i.e from GPlay) being loaded.
                         */
                        emit(
                            ResultSupreme.create(
                                status,
                                Pair(filterWithKeywordSearch(cleanApkResults), true)
                            )
                        )
                    }
                    emitSource(
                        getGplayAndCleanapkCombinedResults(query, authData, cleanApkResults).map {
                            /*
                             * We are assuming that there will be no timeout here.
                             * If there had to be any timeout, it would already have happened
                             * while fetching package specific results.
                             */
                            ResultSupreme.Success(
                                Pair(
                                    filterWithKeywordSearch(it.first),
                                    it.second
                                )
                            )
    private suspend fun getCleanApkPackageResult(
        query: String,
    ): FusedApp? {
        getCleanapkSearchResult(query).let {
            if (it.isSuccess() && it.data!!.package_name.isNotBlank()) {
                return it.data!!
            }
                    )
        }
                APP_TYPE_OPEN -> {
                    val status = runCodeBlockWithTimeout({
                        cleanApkResults.addAll(getCleanAPKSearchResults(query))
                    })
                    /*
                     * Send false in pair to signal no more results to load, as only cleanapk
                     * results are fetched, we don't have to wait for GPlay results.
                     */
                    emit(
                        ResultSupreme.create(
                            status,
                            Pair(filterWithKeywordSearch(cleanApkResults), false)
                        )
                    )
        return null
    }
                APP_TYPE_PWA -> {
                    val status = runCodeBlockWithTimeout({
                        cleanApkResults.addAll(
                            getCleanAPKSearchResults(
                                query,
                                CleanAPKInterface.APP_SOURCE_ANY,
                                CleanAPKInterface.APP_TYPE_PWA
                            )
                        )
                    })
                    /*
                     * Send false in pair to signal no more results to load, as only cleanapk
                     * results are fetched for PWAs.
                     */
                    emit(ResultSupreme.create(status, Pair(cleanApkResults, false)))

    private suspend fun getGplayPackagResult(
        query: String,
        authData: AuthData,
    ): FusedApp? {
        try {
            getApplicationDetails(query, query, authData, Origin.GPLAY).let {
                if (it.second == ResultStatus.OK) {
                    return it.first
                }
            }
        } catch (e: Exception) {
            Timber.e(e)
        }
        return null
    }

    /*
@@ -409,7 +483,13 @@ class FusedAPIImpl @Inject constructor(
        versionCode: Int,
        offerType: Int
    ): String? {
        val list = gPlayAPIRepository.getOnDemandModule(packageName, moduleName, versionCode, offerType, authData)
        val list = gPlayAPIRepository.getOnDemandModule(
            packageName,
            moduleName,
            versionCode,
            offerType,
            authData
        )
        for (element in list) {
            if (element.name == "$moduleName.apk") {
                return element.url
@@ -418,7 +498,6 @@ class FusedAPIImpl @Inject constructor(
        return null
    }


    suspend fun updateFusedDownloadWithDownloadingInfo(
        authData: AuthData,
        origin: Origin,
@@ -832,39 +911,69 @@ class FusedAPIImpl @Inject constructor(
        type: Category.Type,
        authData: AuthData
    ): Pair<ResultStatus, String> {
        var data: Categories? = null
        var apiStatus = ResultStatus.OK
        var errorApplicationCategory = ""

        /*
         * Try within timeout limit for open source native apps categories.
         */
        runCodeBlockWithTimeout({
            data = getOpenSourceCategories()
            data?.let {
                categoriesList.addAll(
                    getFusedCategoryBasedOnCategoryType(
                        it,
        if (preferenceManagerModule.isOpenSourceSelected()) {
            val openSourceCategoryResult = fetchOpenSourceCategories(type)
            categoriesList.addAll(openSourceCategoryResult.second)
            apiStatus = openSourceCategoryResult.first
            errorApplicationCategory = openSourceCategoryResult.third
        }

        if (preferenceManagerModule.isPWASelected()) {
            val pwaCategoriesResult = fetchPWACategories(type)
            categoriesList.addAll(pwaCategoriesResult.second)
            apiStatus = pwaCategoriesResult.first
            errorApplicationCategory = pwaCategoriesResult.third
        }

        if (preferenceManagerModule.isGplaySelected()) {
            val gplayCategoryResult = fetchGplayCategories(
                type,
                        AppTag.OpenSource(context.getString(R.string.open_source))
                    )
                authData
            )
            categoriesList.addAll(gplayCategoryResult.second)
            apiStatus = gplayCategoryResult.first
            errorApplicationCategory = gplayCategoryResult.third
        }

        return Pair(apiStatus, errorApplicationCategory)
    }

    private suspend fun FusedAPIImpl.fetchGplayCategories(
        type: Category.Type,
        authData: AuthData,
    ): Triple<ResultStatus, List<FusedCategory>, String> {
        var errorApplicationCategory = ""
        var apiStatus = ResultStatus.OK
        val categoryList = mutableListOf<FusedCategory>()
        runCodeBlockWithTimeout({
            val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app ->
                val category = app.transformToFusedCategory()
                updateCategoryDrawable(category, app)
                category
            }
            categoryList.addAll(playResponse)
        }, {
            errorApplicationCategory = APP_TYPE_OPEN
            errorApplicationCategory = APP_TYPE_ANY
            apiStatus = ResultStatus.TIMEOUT
        }, {
            errorApplicationCategory = APP_TYPE_OPEN
            errorApplicationCategory = APP_TYPE_ANY
            apiStatus = ResultStatus.UNKNOWN
        })
        return Triple(apiStatus, categoryList, errorApplicationCategory)
    }

        /*
         * Try within timeout limit to get PWA categories
         */
    private suspend fun FusedAPIImpl.fetchPWACategories(
        type: Category.Type,
    ): Triple<ResultStatus, List<FusedCategory>, String> {
        var errorApplicationCategory = ""
        var apiStatus: ResultStatus = ResultStatus.OK
        val fusedCategoriesList = mutableListOf<FusedCategory>()
        runCodeBlockWithTimeout({
            data = getPWAsCategories()
            data?.let {
                categoriesList.addAll(
            getPWAsCategories()?.let {
                fusedCategoriesList.addAll(
                    getFusedCategoryBasedOnCategoryType(
                        it, type, AppTag.PWA(context.getString(R.string.pwa))
                    )
@@ -877,26 +986,33 @@ class FusedAPIImpl @Inject constructor(
            errorApplicationCategory = APP_TYPE_PWA
            apiStatus = ResultStatus.UNKNOWN
        })
        return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory)
    }

        /*
         * Try within timeout limit to get native app categories from Play Store
         */
    private suspend fun FusedAPIImpl.fetchOpenSourceCategories(
        type: Category.Type,
    ): Triple<ResultStatus, List<FusedCategory>, String> {
        var errorApplicationCategory = ""
        var apiStatus: ResultStatus = ResultStatus.OK
        val fusedCategoryList = mutableListOf<FusedCategory>()
        runCodeBlockWithTimeout({
            val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app ->
                val category = app.transformToFusedCategory()
                updateCategoryDrawable(category, app)
                category
            getOpenSourceCategories()?.let {
                fusedCategoryList.addAll(
                    getFusedCategoryBasedOnCategoryType(
                        it,
                        type,
                        AppTag.OpenSource(context.getString(R.string.open_source))
                    )
                )
            }
            categoriesList.addAll(playResponse)
        }, {
            errorApplicationCategory = APP_TYPE_ANY
            errorApplicationCategory = APP_TYPE_OPEN
            apiStatus = ResultStatus.TIMEOUT
        }, {
            errorApplicationCategory = APP_TYPE_ANY
            errorApplicationCategory = APP_TYPE_OPEN
            apiStatus = ResultStatus.UNKNOWN
        })

        return Pair(apiStatus, errorApplicationCategory)
        return Triple(apiStatus, fusedCategoryList, errorApplicationCategory)
    }

    /**
@@ -1077,24 +1193,6 @@ class FusedAPIImpl @Inject constructor(
        return list
    }

    /*
     * Function to return a livedata with value from cleanapk and Google Play store combined.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171
     */
    private fun getGplayAndCleanapkCombinedResults(
        query: String,
        authData: AuthData,
        cleanApkResults: List<FusedApp>
    ): LiveData<Pair<List<FusedApp>, Boolean>> {
        val localList = ArrayList<FusedApp>(cleanApkResults)
        return getGplaySearchResults(query, authData).map { pair ->
            Pair(
                localList.apply { addAll(pair.first) }.distinctBy { it.package_name },
                pair.second
            )
        }
    }

    private fun getGplaySearchResults(
        query: String,
        authData: AuthData
@@ -1112,9 +1210,9 @@ class FusedAPIImpl @Inject constructor(
     * Home screen-related internal functions
     */

    private suspend fun generateCleanAPKHome(home: Home, prefType: String): List<FusedHome> {
    private suspend fun generateCleanAPKHome(home: Home, appType: String): List<FusedHome> {
        val list = mutableListOf<FusedHome>()
        val headings = if (prefType == APP_TYPE_OPEN) {
        val headings = if (appType == APP_TYPE_OPEN) {
            mapOf(
                "top_updated_apps" to context.getString(R.string.top_updated_apps),
                "top_updated_games" to context.getString(R.string.top_updated_games),
@@ -1203,7 +1301,10 @@ class FusedAPIImpl @Inject constructor(
                }
            }
        }
        return list
        return list.map {
            it.source = appType
            it
        }
    }

    private suspend fun fetchGPlayHome(authData: AuthData): List<FusedHome> {
+2 −1
Original line number Diff line number Diff line
@@ -20,5 +20,6 @@ package foundation.e.apps.api.fused.data

data class FusedHome(
    val title: String = String(),
    val list: List<FusedApp> = emptyList()
    val list: List<FusedApp> = emptyList(),
    var source: String = String()
)
+17 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@
package foundation.e.apps.home.model

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
@@ -26,6 +27,8 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import foundation.e.apps.AppInfoFetchViewModel
import foundation.e.apps.MainActivityViewModel
import foundation.e.apps.R
import foundation.e.apps.api.fused.FusedAPIImpl
import foundation.e.apps.api.fused.FusedAPIInterface
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.api.fused.data.FusedHome
@@ -70,6 +73,20 @@ class HomeParentRVAdapter(
        homeChildRVAdapter.setData(fusedHome.list)

        holder.binding.titleTV.text = fusedHome.title

        when (fusedHome.source) {
            FusedAPIImpl.APP_TYPE_OPEN -> {
                holder.binding.categoryTag.visibility = View.VISIBLE
                holder.binding.categoryTag.text = holder.binding.root.context.getString(R.string.open_source)
            }
            FusedAPIImpl.APP_TYPE_PWA -> {
                holder.binding.categoryTag.visibility = View.VISIBLE
                holder.binding.categoryTag.text = holder.binding.root.context.getString(R.string.pwa)
            }
            else -> {
                holder.binding.categoryTag.visibility = View.GONE
            }
        }
        holder.binding.childRV.apply {
            recycledViewPool.setMaxRecycledViews(0, 0)
            adapter = homeChildRVAdapter
Loading