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

Commit 3e6ad340 authored by Hasib Prince's avatar Hasib Prince
Browse files

Merge branch '6653-category_apps' into 'main'

6653 category apps

See merge request !319
parents 86a4d4f5 89a60fcd
Loading
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -151,8 +151,7 @@ dependencies {
    api files('libs/splitinstall-lib.jar')

    implementation 'foundation.e.lib:telemetry:0.0.9-alpha'

    implementation 'foundation.e:gplayapi:3.0.1'
    implementation 'foundation.e:gplayapi:3.0.1-1'
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.fragment:fragment-ktx:1.5.6'
+11 −1
Original line number Diff line number Diff line
@@ -20,5 +20,15 @@ package foundation.e.apps.data.enums
enum class Source {
    GPLAY,
    OPEN,
    PWA,
    PWA;

    companion object {
        fun fromString(source: String): Source {
            return when (source) {
                "Open Source" -> OPEN
                "PWA" -> PWA
                else -> GPLAY
            }
        }
    }
}
+9 −334
Original line number Diff line number Diff line
@@ -20,14 +20,13 @@ package foundation.e.apps.data.fused

import androidx.lifecycle.LiveData
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.enums.AppTag
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.Origin
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.fused.data.FusedApp
import foundation.e.apps.data.fused.data.FusedCategory
@@ -40,44 +39,6 @@ import javax.inject.Singleton
@Singleton
class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi) {

    var streamBundle = StreamBundle()
        private set
    var streamCluster = StreamCluster()
        private set

    var clusterPointer = 0
        private set

    /**
     * Variable denoting if we can call [getNextStreamCluster] to get a new StreamBundle.
     *
     * Initially set to true, so that we can get the first StreamBundle.
     * Once the first StreamBundle is fetched, this variable value is same
     * as [streamBundle].hasNext().
     *
     * For more explanation on how [streamBundle] and [streamCluster] work, look at the
     * documentation in [getNextDataSet].
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     */
    var hasNextStreamBundle = true
        private set

    /**
     * Variable denoting if we can call [getNextStreamCluster] to get a new StreamCluster.
     *
     * Initially set to false so that we get a StreamBundle first, because initially
     * [streamCluster] is empty. Once [streamBundle] is fetched and [getAdjustedFirstCluster]
     * is called, this variable value is same as [streamCluster].hasNext().
     *
     * For more explanation on how [streamBundle] and [streamCluster] work, look at the
     * documentation in [getNextDataSet].
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     */
    var hasNextStreamCluster = false
        private set

    suspend fun getHomeScreenData(authData: AuthData): LiveData<ResultSupreme<List<FusedHome>>> {
        return fusedAPIImpl.getHomeScreenData(authData)
    }
@@ -98,13 +59,6 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi)
        return fusedAPIImpl.getApplicationDetails(packageNameList, authData, origin)
    }

    suspend fun filterRestrictedGPlayApps(
        authData: AuthData,
        appList: List<App>,
    ): ResultSupreme<List<FusedApp>> {
        return fusedAPIImpl.filterRestrictedGPlayApps(authData, appList)
    }

    suspend fun getAppFilterLevel(fusedApp: FusedApp, authData: AuthData?): FilterLevel {
        return fusedAPIImpl.getAppFilterLevel(fusedApp, authData)
    }
@@ -162,39 +116,16 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi)
        return fusedAPIImpl.getSearchResults(query, authData)
    }

    suspend fun getNextStreamBundle(
        homeUrl: String,
        currentStreamBundle: StreamBundle,
    ): ResultSupreme<StreamBundle> {
        return fusedAPIImpl.getNextStreamBundle(homeUrl, currentStreamBundle).apply {
            if (isValidData()) streamBundle = data!!
            hasNextStreamBundle = streamBundle.hasNext()
            clusterPointer = 0
        }
    }

    suspend fun getAdjustedFirstCluster(
        streamBundle: StreamBundle,
        pointer: Int = 0,
    ): ResultSupreme<StreamCluster> {
        return fusedAPIImpl.getAdjustedFirstCluster(streamBundle, pointer)
    }

    suspend fun getNextStreamCluster(
        currentStreamCluster: StreamCluster,
    ): ResultSupreme<StreamCluster> {
        return fusedAPIImpl.getNextStreamCluster(currentStreamCluster)
    }

    suspend fun getAppsListBasedOnCategory(
        authData: AuthData,
        category: String,
        browseUrl: String,
        source: String
    ): ResultSupreme<List<FusedApp>> {
        pageUrl: String?,
        source: Source
    ): ResultSupreme<Pair<List<FusedApp>, String>> {
        return when (source) {
            "Open Source" -> fusedAPIImpl.getOpenSourceApps(category)
            "PWA" -> fusedAPIImpl.getPWAApps(category)
            else -> fusedAPIImpl.getPlayStoreApps(browseUrl)
            Source.OPEN -> fusedAPIImpl.getOpenSourceApps(category)
            Source.PWA -> fusedAPIImpl.getPWAApps(category)
            else -> fusedAPIImpl.getGplayAppsByCategory(authData, category, pageUrl)
        }
    }

@@ -215,261 +146,5 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi)
    fun isAnyAppInstallStatusChanged(currentList: List<FusedApp>) =
        fusedAPIImpl.isAnyAppInstallStatusChanged(currentList)

    suspend fun getAppList(
        category: String,
        browseUrl: String,
        authData: AuthData,
        source: String
    ): ResultSupreme<List<FusedApp>> {
        return if (source == "Open Source" || source == "PWA") {
            getAppsListBasedOnCategory(
                category,
                browseUrl,
                source
            )
        } else {
            getNextDataSet(authData, browseUrl).apply {
                addPlaceHolderAppIfNeeded(this)
            }
        }
    }

    /**
     * @return a Pair,
     * 1. first item is the data
     * 1. second item is item count is changed or not
     */
    suspend fun loadMore(authData: AuthData, browseUrl: String): Pair<ResultSupreme<List<FusedApp>>, Boolean> {
        val lastCount: Int = streamCluster.clusterAppList.size
        val result = getNextDataSet(authData, browseUrl)
        val newCount = streamCluster.clusterAppList.size
        return Pair(result, lastCount != newCount)
    }

    /**
     * This is how the logic works:
     *
     * StreamBundles are obtained from "browseUrls".
     * Each StreamBundle can contain
     * - some StreamClusters,
     * - point to a following StreamBundle with "streamNextPageUrl"
     *   (checked by StreamBundle.hasNext())
     * Each StreamCluster contain
     * - apps to display
     * - a "clusterBrowseUrl"
     * - can point to a following StreamCluster with new app data using "clusterNextPageUrl"
     *   (checked by StreamCluster.hasNext())
     *
     * -- browseUrl
     *    |
     *    StreamBundle 1 (streamNextPageUrl points to StreamBundle 2)
     *        StreamCluster 1 -> StreamCluster 1.1 -> StreamCluster 1.2 ....
     *        StreamCluster 2 -> StreamCluster 2.1 -> StreamCluster 2.2 ....
     *        StreamCluster 3 -> StreamCluster 3.1 -> StreamCluster 3.2 ....
     *    StreamBundle 2
     *        StreamCluster 4 -> ...
     *        StreamCluster 5 -> ...
     *
     *
     * - "browseUrl": looks like: homeV2?cat=SOCIAL&c=3
     * - "clusterBrowseUrl" (not used here): looks like:
     *   getBrowseStream?ecp=ChWiChIIARIGU09DSUFMKgIIB1ICCAE%3D
     *   getBrowseStream?ecp=CjOiCjAIARIGU09DSUFMGhwKFnJlY3NfdG9waWNfRjkxMjZNYVJ6S1UQOxgDKgIIB1ICCAI%3D
     * - "clusterNextPageUrl" (not directly used here): looks like:
     *   getCluster?enpt=CkCC0_-4AzoKMfqegZ0DKwgIEKGz2kgQuMifuAcQ75So0QkQ6Ijz6gwQzvel8QQQprGBmgUQz938owMQyIeljYQwEAcaFaIKEggBEgZTT0NJQUwqAggHUgIIAQ&n=20
     *
     * ========== Working logic ==========
     *
     * 1. [streamCluster] accumulates all data from all subsequent network calls.
     * Its "clusterNextPageUrl" does point to the next StreamCluster, but its "clusterAppList"
     * contains accumulated data of all previous network calls.
     *
     * 2. [streamBundle] is the same value received from [getNextStreamBundle].
     *
     * 3. Initially [hasNextStreamCluster] is false, denoting [streamCluster] is empty.
     * Initially [clusterPointer] = 0, [streamBundle].streamClusters.size = 0,
     * hence 2nd case also does not execute.
     * However, initially [hasNextStreamBundle] is true, thus [getNextStreamBundle] is called,
     * fetching the first StreamBundle and storing the data in [streamBundle], and getting the first
     * StreamCluster data using [getAdjustedFirstCluster].
     *
     * NOTE: [getAdjustedFirstCluster] is used to fetch StreamCluster 1, 2, 3 .. in the above
     * diagram with help of [clusterPointer]. For subsequent StreamCluster 1.1, 1.2 .. 2.1 ..
     * [getNextStreamCluster] is used.
     *
     * 4. From now onwards,
     * - [hasNextStreamBundle] is as good as [streamBundle].hasNext()
     * - [hasNextStreamCluster] is as good as [streamCluster].hasNext()
     *
     * 5.1. When this method is again called when list reaches the end while scrolling on the UI,
     * if [hasNextStreamCluster] is true, we will get the next StreamCluster under the current
     * StreamBundle object. Once the last StreamCluster is reached, [hasNextStreamCluster] is
     * false, we move to the next case.
     *
     * 5.2. In the step 5.1 we have been traversing along the path StreamCluster 1 -> 1.1 -> 1.2 ..
     * Once that path reaches an end, we need to jump to StreamCluster 2 -> 2.1 -> 2.2 ..
     * This is achieved by the second condition using [clusterPointer]. We increment the
     * pointer and call [getAdjustedFirstCluster] again to start from StreamCluster 2.
     *
     * 5.3. Once we no longer have any more beginning StreamClusters, i.e
     * [clusterPointer] exceeds [streamBundle].streamClusters size, the second condition no
     * longer holds. Now we should try to go to a different StreamBundle.
     * Using the above diagram, we move to StreamBundle 1 -> 2.
     * We check [hasNextStreamBundle]. If that is true, we load the next StreamBundle.
     * This also fetches the first StreamCluster of this bundle, thus re-initialising both
     * [hasNextStreamCluster] and [hasNextStreamBundle].
     *
     * 6. Once we reach the end of all StreamBundles and all StreamClusters, now calling
     * this method makes no network calls.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     */

    private suspend fun getNextDataSet(
        authData: AuthData,
        browseUrl: String,
    ): ResultSupreme<List<FusedApp>> {
        if (hasNextStreamCluster) {
            getNextStreamCluster(authData).run {
                if (!isSuccess()) {
                    return ResultSupreme.replicate(this, listOf())
                }
            }
        } else if (clusterPointer < streamBundle.streamClusters.size) {
            ++clusterPointer
            getAdjustedFirstCluster(authData).run {
                if (!isSuccess()) {
                    return ResultSupreme.replicate(this, listOf())
                }
            }
        } else if (hasNextStreamBundle) {
            getNextStreamBundle(browseUrl).run {
                if (!isSuccess()) {
                    return ResultSupreme.replicate(this, listOf())
                }
                getAdjustedFirstCluster(authData).run {
                    if (!isSuccess()) {
                        return ResultSupreme.replicate(this, listOf())
                    }
                }
            }
        }
        return filterRestrictedGPlayApps(authData, streamCluster.clusterAppList)
    }

    /**
     * Add a placeholder app at the end if more data can be loaded.
     * "Placeholder" app shows a simple progress bar in the RecyclerView, indicating that
     * more apps are being loaded.
     *
     * Note that it mutates the [ResultSupreme] object passed to it.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     *
     * @param result object from [getNextDataSet]. Data of this object will be updated
     * if [canLoadMore] is true.
     *
     * @return true if a placeholder app was added, false otherwise.
     */
    fun addPlaceHolderAppIfNeeded(result: ResultSupreme<List<FusedApp>>): Boolean {
        result.apply {
            if (isSuccess() && canLoadMore()) {
                // Add an empty app at the end if more data can be loaded on scroll
                val newData = data!!.toMutableList()
                newData.add(FusedApp(isPlaceHolder = true))
                setData(newData)
                return true
            }
        }
        return false
    }

    /**
     * Get the first StreamBundle object from the category browseUrl, or the subsequent
     * StreamBundle objects from the "streamNextPageUrl" of current [streamBundle].
     * Also resets the [clusterPointer] to 0.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     *
     * @see getNextDataSet
     */
    private suspend fun getNextStreamBundle(
        browseUrl: String,
    ): ResultSupreme<StreamBundle> {
        return getNextStreamBundle(browseUrl, streamBundle).apply {
            if (isValidData()) streamBundle = data!!
            hasNextStreamBundle = streamBundle.hasNext()
            clusterPointer = 0
        }
    }

    /**
     * The first StreamCluster inside [streamBundle] may not have a "clusterNextPageUrl".
     * This method tries to fix that.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     *
     * @see getNextDataSet
     */
    private suspend fun getAdjustedFirstCluster(
        authData: AuthData,
    ): ResultSupreme<StreamCluster> {
        return getAdjustedFirstCluster(streamBundle, clusterPointer)
            .apply {
                if (isValidData()) addNewClusterData(this.data!!)
            }
    }

    /**
     * Get all subsequent StreamCluster of the current [streamBundle].
     * Accumulate the data in [streamCluster].
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     *
     * @see getNextDataSet
     */
    private suspend fun getNextStreamCluster(
        authData: AuthData,
    ): ResultSupreme<StreamCluster> {
        return getNextStreamCluster(streamCluster).apply {
            if (isValidData()) addNewClusterData(this.data!!)
        }
    }

    /**
     * Method to add clusterAppList of [newCluster] to [streamCluster],
     * but properly point to next StreamCluster.
     * Also updates [hasNextStreamCluster].
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     */
    private fun addNewClusterData(newCluster: StreamCluster) {
        newCluster.run {
            streamCluster.clusterAppList.apply {
                val addedList = this + newCluster.clusterAppList
                clear()
                addAll(addedList.distinctBy { it.packageName })
            }
            streamCluster.clusterNextPageUrl = this.clusterNextPageUrl
            streamCluster.clusterBrowseUrl = this.clusterBrowseUrl
        }
        hasNextStreamCluster = newCluster.hasNext()
    }

    /**
     * Function is used to check if we can load more data.
     * It is also used to show a loading progress bar at the end of the list.
     */
    fun canLoadMore(): Boolean =
        hasNextStreamCluster || clusterPointer < streamBundle.streamClusters.size || hasNextStreamBundle

    fun clearData() {
        streamCluster = StreamCluster()
        streamBundle = StreamBundle()
        hasNextStreamBundle = true
        hasNextStreamCluster = false
        clusterPointer = 0
    }

    fun isOpenSourceSelected() = fusedAPIImpl.isOpenSourceSelected()
}
+4 −22
Original line number Diff line number Diff line
@@ -4,8 +4,6 @@ import androidx.lifecycle.LiveData
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.cleanapk.data.download.Download
import foundation.e.apps.data.enums.FilterLevel
@@ -82,27 +80,9 @@ interface FusedApi {

    suspend fun getOSSDownloadInfo(id: String, version: String?): Response<Download>

    suspend fun getPWAApps(category: String): ResultSupreme<List<FusedApp>>
    suspend fun getPWAApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>>

    suspend fun getOpenSourceApps(category: String): ResultSupreme<List<FusedApp>>

    suspend fun getNextStreamBundle(
        homeUrl: String,
        currentStreamBundle: StreamBundle,
    ): ResultSupreme<StreamBundle>

    suspend fun getAdjustedFirstCluster(
        streamBundle: StreamBundle,
        pointer: Int = 0,
    ): ResultSupreme<StreamCluster>

    suspend fun getNextStreamCluster(
        currentStreamCluster: StreamCluster,
    ): ResultSupreme<StreamCluster>

    suspend fun getPlayStoreApps(
        browseUrl: String,
    ): ResultSupreme<List<FusedApp>>
    suspend fun getOpenSourceApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>>

    /*
        * Function to search cleanapk using package name.
@@ -178,4 +158,6 @@ interface FusedApi {

    fun isAnyAppInstallStatusChanged(currentList: List<FusedApp>): Boolean
    fun isOpenSourceSelected(): Boolean

    suspend fun getGplayAppsByCategory(authData: AuthData, category: String, pageUrl: String?): ResultSupreme<Pair<List<FusedApp>, String>>
}
+36 −54
Original line number Diff line number Diff line
@@ -29,7 +29,6 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
@@ -522,6 +521,7 @@ class FusedApiImpl @Inject constructor(
                downloadInfo?.download_data?.download_link?.let { list.add(it) }
                fusedDownload.signature = downloadInfo?.download_data?.signature ?: ""
            }

            Origin.GPLAY -> {
                val downloadList =
                    gplayRepository.getDownloadInfo(
@@ -532,6 +532,7 @@ class FusedApiImpl @Inject constructor(
                fusedDownload.files = downloadList
                list.addAll(downloadList.map { it.url })
            }

            Origin.GITLAB -> {
            }
        }
@@ -541,7 +542,7 @@ class FusedApiImpl @Inject constructor(
    override suspend fun getOSSDownloadInfo(id: String, version: String?) =
        (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo(id, version)

    override suspend fun getPWAApps(category: String): ResultSupreme<List<FusedApp>> {
    override suspend fun getPWAApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>> {
        val list = mutableListOf<FusedApp>()
        val status = runCodeBlockWithTimeout({
            val response = getPWAAppsResponse(category)
@@ -552,10 +553,10 @@ class FusedApiImpl @Inject constructor(
                list.add(it)
            }
        })
        return ResultSupreme.create(status, list)
        return ResultSupreme.create(status, Pair(list, ""))
    }

    override suspend fun getOpenSourceApps(category: String): ResultSupreme<List<FusedApp>> {
    override suspend fun getOpenSourceApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>> {
        val list = mutableListOf<FusedApp>()
        val status = runCodeBlockWithTimeout({
            val response = getOpenSourceAppsResponse(category)
@@ -566,56 +567,7 @@ class FusedApiImpl @Inject constructor(
                list.add(it)
            }
        })
        return ResultSupreme.create(status, list)
    }

    override suspend fun getNextStreamBundle(
        homeUrl: String,
        currentStreamBundle: StreamBundle,
    ): ResultSupreme<StreamBundle> {
        var streamBundle = StreamBundle()
        val status = runCodeBlockWithTimeout({
            streamBundle =
                gplayRepository.getAppsByCategory(homeUrl, currentStreamBundle) as StreamBundle
        })
        return ResultSupreme.create(status, streamBundle)
    }

    override suspend fun getAdjustedFirstCluster(
        streamBundle: StreamBundle,
        pointer: Int,
    ): ResultSupreme<StreamCluster> {
        var streamCluster = StreamCluster()
        val status = runCodeBlockWithTimeout({
            streamCluster =
                gplayRepository.getAppsByCategory("", Pair(streamBundle, pointer)) as StreamCluster
        })
        return ResultSupreme.create(status, streamCluster)
    }

    override suspend fun getNextStreamCluster(
        currentStreamCluster: StreamCluster,
    ): ResultSupreme<StreamCluster> {
        var streamCluster = StreamCluster()
        val status = runCodeBlockWithTimeout({
            streamCluster =
                gplayRepository.getAppsByCategory("", currentStreamCluster) as StreamCluster
        })
        return ResultSupreme.create(status, streamCluster)
    }

    override suspend fun getPlayStoreApps(
        browseUrl: String,
    ): ResultSupreme<List<FusedApp>> {
        val list = mutableListOf<FusedApp>()
        val status = runCodeBlockWithTimeout({
            list.addAll(
                (gplayRepository.getAppsByCategory(browseUrl) as List<App>).map { app ->
                    app.transformToFusedApp()
                }
            )
        })
        return ResultSupreme.create(status, list)
        return ResultSupreme.create(status, Pair(list, ""))
    }

    /*
@@ -1045,6 +997,7 @@ class FusedApiImpl @Inject constructor(
            CategoryType.APPLICATION -> {
                getAppsCategoriesAsFusedCategory(categories, tag)
            }

            CategoryType.GAMES -> {
                getGamesCategoriesAsFusedCategory(categories, tag)
            }
@@ -1210,6 +1163,7 @@ class FusedApiImpl @Inject constructor(
                        list.add(FusedHome(value, home.top_updated_apps))
                    }
                }

                "top_updated_games" -> {
                    if (home.top_updated_games.isNotEmpty()) {
                        home.top_updated_games.forEach {
@@ -1220,6 +1174,7 @@ class FusedApiImpl @Inject constructor(
                        list.add(FusedHome(value, home.top_updated_games))
                    }
                }

                "popular_apps" -> {
                    if (home.popular_apps.isNotEmpty()) {
                        home.popular_apps.forEach {
@@ -1230,6 +1185,7 @@ class FusedApiImpl @Inject constructor(
                        list.add(FusedHome(value, home.popular_apps))
                    }
                }

                "popular_games" -> {
                    if (home.popular_games.isNotEmpty()) {
                        home.popular_games.forEach {
@@ -1240,6 +1196,7 @@ class FusedApiImpl @Inject constructor(
                        list.add(FusedHome(value, home.popular_games))
                    }
                }

                "popular_apps_in_last_24_hours" -> {
                    if (home.popular_apps_in_last_24_hours.isNotEmpty()) {
                        home.popular_apps_in_last_24_hours.forEach {
@@ -1250,6 +1207,7 @@ class FusedApiImpl @Inject constructor(
                        list.add(FusedHome(value, home.popular_apps_in_last_24_hours))
                    }
                }

                "popular_games_in_last_24_hours" -> {
                    if (home.popular_games_in_last_24_hours.isNotEmpty()) {
                        home.popular_games_in_last_24_hours.forEach {
@@ -1260,6 +1218,7 @@ class FusedApiImpl @Inject constructor(
                        list.add(FusedHome(value, home.popular_games_in_last_24_hours))
                    }
                }

                "discover" -> {
                    if (home.discover.isNotEmpty()) {
                        home.discover.forEach {
@@ -1447,4 +1406,27 @@ class FusedApiImpl @Inject constructor(
    }

    override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected()
    override suspend fun getGplayAppsByCategory(
        authData: AuthData,
        category: String,
        pageUrl: String?
    ): ResultSupreme<Pair<List<FusedApp>, String>> {
        var fusedAppList: MutableList<FusedApp> = mutableListOf()
        var nextPageUrl = ""

        val status = runCodeBlockWithTimeout({
            val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster
            val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList)
            filteredAppList.data?.let {
                fusedAppList = it.toMutableList()
            }

            nextPageUrl = streamCluster.clusterNextPageUrl
            if (!nextPageUrl.isNullOrEmpty()) {
                fusedAppList.add(FusedApp(isPlaceHolder = true))
            }
        })

        return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl))
    }
}
Loading