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

Commit 9cbd5690 authored by Hasib Prince's avatar Hasib Prince
Browse files

Refactoring applicationlistviewmodel (partially done)

parent 5f90d407
Loading
Loading
Loading
Loading
Loading
+259 −1
Original line number Diff line number Diff line
@@ -136,7 +136,11 @@ class FusedAPIRepository @Inject constructor(
        homeUrl: String,
        currentStreamBundle: StreamBundle,
    ): ResultSupreme<StreamBundle> {
        return fusedAPIImpl.getNextStreamBundle(authData, homeUrl, currentStreamBundle)
        return fusedAPIImpl.getNextStreamBundle(authData, homeUrl, currentStreamBundle).apply {
            if (isValidData()) streamBundle = data!!
            hasNextStreamBundle = streamBundle.hasNext()
            clusterPointer = 0
        }
    }

    suspend fun getAdjustedFirstCluster(
@@ -183,4 +187,258 @@ class FusedAPIRepository @Inject constructor(

    fun isAnyAppInstallStatusChanged(currentList: List<FusedApp>) =
        fusedAPIImpl.isAnyAppInstallStatusChanged(currentList)

    /**
     * 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 var streamBundle = StreamBundle()
    private var streamCluster = StreamCluster()

    private var clusterPointer = 0

    /**
     * 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]
     */
    private var hasNextStreamBundle = true

    /**
     * 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]
     */
    private var hasNextStreamCluster = false


    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(authData, browseUrl).run {
                if (!isSuccess()) {
                    return ResultSupreme.replicate(this, listOf())
                }
                getAdjustedFirstCluster(authData).run {
                    if (!isSuccess()) {
                        return ResultSupreme.replicate(this, listOf())
                    }
                }
            }
        }
        return filterRestrictedGPlayApps(authData, streamCluster.clusterAppList).apply {
            addPlaceHolderAppIfNeeded(this)
        }
    }

    /**
     * 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.
     */
    private 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(
        authData: AuthData,
        browseUrl: String,
    ): ResultSupreme<StreamBundle> {
        return getNextStreamBundle(authData, 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(authData, 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(authData, 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
}
+2 −2
Original line number Diff line number Diff line
@@ -88,7 +88,7 @@ class ApplicationListViewModel @Inject constructor(
                    source
                )
            } else {
                getNextDataSet(authData, browseUrl).apply {
                fusedAPIRepository.getNextDataSet(authData, browseUrl).apply {
                    addPlaceHolderAppIfNeeded(this)
                }
            }
@@ -138,7 +138,7 @@ class ApplicationListViewModel @Inject constructor(
        viewModelScope.launch {
            if (!isLoading) {
                val lastCount: Int = streamCluster.clusterAppList.size
                val result = getNextDataSet(authData, browseUrl)
                val result = fusedAPIRepository.getNextDataSet(authData, browseUrl)
                val newCount = streamCluster.clusterAppList.size
                appListLiveData.postValue(result)
                /*