Loading app/build.gradle +1 −2 Original line number Diff line number Diff line Loading @@ -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' Loading app/src/main/java/foundation/e/apps/data/enums/Source.kt +11 −1 Original line number Diff line number Diff line Loading @@ -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 } } } } app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +9 −334 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } Loading @@ -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) } Loading Loading @@ -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) } } Loading @@ -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() } app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +4 −22 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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>> } app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +36 −54 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading @@ -532,6 +532,7 @@ class FusedApiImpl @Inject constructor( fusedDownload.files = downloadList list.addAll(downloadList.map { it.url }) } Origin.GITLAB -> { } } Loading @@ -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) Loading @@ -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) Loading @@ -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, "")) } /* Loading Loading @@ -1045,6 +997,7 @@ class FusedApiImpl @Inject constructor( CategoryType.APPLICATION -> { getAppsCategoriesAsFusedCategory(categories, tag) } CategoryType.GAMES -> { getGamesCategoriesAsFusedCategory(categories, tag) } Loading Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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
app/build.gradle +1 −2 Original line number Diff line number Diff line Loading @@ -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' Loading
app/src/main/java/foundation/e/apps/data/enums/Source.kt +11 −1 Original line number Diff line number Diff line Loading @@ -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 } } } }
app/src/main/java/foundation/e/apps/data/fused/FusedAPIRepository.kt +9 −334 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } Loading @@ -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) } Loading Loading @@ -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) } } Loading @@ -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() }
app/src/main/java/foundation/e/apps/data/fused/FusedApi.kt +4 −22 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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. Loading Loading @@ -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>> }
app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +36 −54 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading @@ -532,6 +532,7 @@ class FusedApiImpl @Inject constructor( fusedDownload.files = downloadList list.addAll(downloadList.map { it.url }) } Origin.GITLAB -> { } } Loading @@ -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) Loading @@ -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) Loading @@ -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, "")) } /* Loading Loading @@ -1045,6 +997,7 @@ class FusedApiImpl @Inject constructor( CategoryType.APPLICATION -> { getAppsCategoriesAsFusedCategory(categories, tag) } CategoryType.GAMES -> { getGamesCategoriesAsFusedCategory(categories, tag) } Loading Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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)) } }