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

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

category with pagination

parent f8056b90
Loading
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -90,8 +90,6 @@ android {

    buildTypes {
        debug {
            versionNameSuffix ".debug"
            applicationIdSuffix ".debug"
            signingConfig signingConfigs.debugConfig
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
@@ -149,10 +147,11 @@ dependencies {

    // TODO: Add splitinstall-lib to a repo https://gitlab.e.foundation/e/os/backlog/-/issues/628
    api files('libs/splitinstall-lib.jar')
    api files('libs/gplayapi-3.0.1.jar')

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

    implementation 'foundation.e:gplayapi:3.0.1'
//    implementation 'foundation.e:gplayapi:3.0.1'
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.fragment:fragment-ktx:1.5.6'
+5 −327
Original line number Diff line number Diff line
@@ -40,44 +40,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 +60,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 +117,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,
        browseUrl: String?,
        source: String
    ): ResultSupreme<List<FusedApp>> {
    ): ResultSupreme<Pair<List<FusedApp>, String>> {
        return when (source) {
            "Open Source" -> fusedAPIImpl.getOpenSourceApps(category)
            "PWA" -> fusedAPIImpl.getPWAApps(category)
            else -> fusedAPIImpl.getPlayStoreApps(browseUrl)
            else -> fusedAPIImpl.getAppsByCategory(authData, category, browseUrl)
        }
    }

@@ -215,261 +147,7 @@ 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 −2
Original line number Diff line number Diff line
@@ -82,9 +82,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 getOpenSourceApps(category: String): ResultSupreme<Pair<List<FusedApp>, String>>

    suspend fun getNextStreamBundle(
        homeUrl: String,
@@ -178,4 +178,6 @@ interface FusedApi {

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

    suspend fun getAppsByCategory(authData: AuthData, category: String, nextPageUrl: String?): ResultSupreme<Pair<List<FusedApp>, String>>
}
+33 −5
Original line number Diff line number Diff line
@@ -522,6 +522,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 +533,7 @@ class FusedApiImpl @Inject constructor(
                fusedDownload.files = downloadList
                list.addAll(downloadList.map { it.url })
            }

            Origin.GITLAB -> {
            }
        }
@@ -541,7 +543,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 +554,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,7 +568,7 @@ class FusedApiImpl @Inject constructor(
                list.add(it)
            }
        })
        return ResultSupreme.create(status, list)
        return ResultSupreme.create(status, Pair(list, ""))
    }

    override suspend fun getNextStreamBundle(
@@ -1045,6 +1047,7 @@ class FusedApiImpl @Inject constructor(
            CategoryType.APPLICATION -> {
                getAppsCategoriesAsFusedCategory(categories, tag)
            }

            CategoryType.GAMES -> {
                getGamesCategoriesAsFusedCategory(categories, tag)
            }
@@ -1210,6 +1213,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 +1224,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 +1235,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 +1246,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 +1257,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 +1268,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 +1456,23 @@ class FusedApiImpl @Inject constructor(
    }

    override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected()
    override suspend fun getAppsByCategory(
        authData: AuthData,
        category: String,
        pageUrl: String?
    ): ResultSupreme<Pair<List<FusedApp>, String>> {
        var fusedAppList: List<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
            }
            nextPageUrl = streamCluster.clusterNextPageUrl
        })

        return ResultSupreme.create(status, Pair(fusedAppList, nextPageUrl))
    }
}
+0 −446

File deleted.

Preview size limit exceeded, changes collapsed.

Loading