Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +10 −0 Original line number Diff line number Diff line Loading @@ -227,6 +227,16 @@ class FusedAPIImpl @Inject constructor( } } suspend fun getPlayStoreAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { return gPlayAPIRepository.listAppCategoryUrls(browseUrl, authData) } suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<FusedApp>, String> { return gPlayAPIRepository.getAppsAndNextClusterUrl(browseUrl, authData).let { Pair(it.first.map { app -> app.transformToFusedApp() }, it.second) } } suspend fun getApplicationDetails( packageNameList: List<String>, authData: AuthData, Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +8 −0 Original line number Diff line number Diff line Loading @@ -94,6 +94,14 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.listApps(category, browseUrl, authData) } suspend fun getPlayStoreAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { return fusedAPIImpl.getPlayStoreAppCategoryUrls(browseUrl, authData) } suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<FusedApp>, String> { return fusedAPIImpl.getAppsAndNextClusterUrl(browseUrl, authData) } suspend fun getAppsListBasedOnCategory( category: String, browseUrl: String, Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +131 −11 Original line number Diff line number Diff line Loading @@ -19,21 +19,20 @@ package foundation.e.apps.api.gplay 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.Category import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.* import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AuthValidator import com.aurora.gplayapi.helpers.CategoryHelper import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.TopChartsHelper import foundation.e.apps.api.gplay.token.TokenRepository import foundation.e.apps.api.gplay.utils.GPlayHttpClient import foundation.e.apps.utils.modules.DataStoreModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import javax.inject.Inject Loading Loading @@ -80,9 +79,18 @@ class GPlayAPIImpl @Inject constructor( // Fetch more results in case the given result is a promoted app if (searchData.size == 1) { val searchBundle = searchHelper.next(searchResult.subBundles) val bundleSet: MutableSet<SearchBundle.SubBundle> = searchResult.subBundles do { val searchBundle = searchHelper.next(bundleSet) if (searchBundle.appList.isNotEmpty()) { searchData.addAll(searchBundle.appList) } bundleSet.apply { clear() addAll(searchBundle.subBundles) } } while (bundleSet.isNotEmpty()) } } return searchData } Loading Loading @@ -141,14 +149,126 @@ class GPlayAPIImpl @Inject constructor( return categoryList } /** * Get list of "clusterBrowseUrl" which can be used to get [StreamCluster] objects which * have "clusterNextPageUrl" to get subsequent [StreamCluster] objects. * * * -- browseUrl * | * StreamBundle 1 (streamNextPageUrl points to StreamBundle 2) * clusterBrowseUrl 1 -> clusterNextPageUrl 1.1 -> clusterNextPageUrl -> 1.2 .... * clusterBrowseUrl 2 -> clusterNextPageUrl 2.1 -> clusterNextPageUrl -> 2.2 .... * clusterBrowseUrl 3 -> clusterNextPageUrl 3.1 -> clusterNextPageUrl -> 3.2 .... * StreamBundle 2 * clusterBroseUrl 4 -> ... * clusterBroseUrl 5 -> ... * * This function returns the clusterBrowseUrls 1,2,3,4,5... */ suspend fun listAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { val urlList = mutableListOf<String>() withContext(Dispatchers.IO) { supervisorScope { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) var streamBundle: StreamBundle var nextStreamBundleUrl = browseUrl do { streamBundle = categoryHelper.getSubCategoryBundle(nextStreamBundleUrl) val streamClusters = streamBundle.streamClusters.values urlList.addAll(streamClusters.map { it.clusterBrowseUrl }) nextStreamBundleUrl = streamBundle.streamNextPageUrl } while (nextStreamBundleUrl.isNotBlank()) } } return urlList.distinct().filter { it.isNotBlank() } } /** * Accept a [browseUrl] of type "clusterBrowseUrl" or "clusterNextPageUrl". * Fetch a StreamCluster from the [browseUrl] and return pair of: * - List od apps to display. * - String url "clusterNextPageUrl" pointing to next StreamCluster. This can be blank (not null). */ suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<App>, String> { val streamCluster: StreamCluster withContext(Dispatchers.IO) { supervisorScope { val streamHelper = StreamHelper(authData).using(gPlayHttpClient) val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl) streamCluster = if (browseResponse.contentsUrl.isNotEmpty()) { streamHelper.getNextStreamCluster(browseResponse.contentsUrl) } else if (browseResponse.hasBrowseTab()) { streamHelper.getNextStreamCluster(browseResponse.browseTab.listUrl) } else { StreamCluster() } } } return Pair(streamCluster.clusterAppList, streamCluster.clusterNextPageUrl) } suspend fun listApps(browseUrl: String, authData: AuthData): List<App> { val list = mutableListOf<App>() withContext(Dispatchers.IO) { supervisorScope { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) val streamClusters = categoryHelper.getSubCategoryBundle(browseUrl).streamClusters var streamBundle: StreamBundle var nextStreamBundleUrl = browseUrl /* * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 * * Logic: We start with the browseUrl. * When we call getSubCategoryBundle(), we get a new StreamBundle object, having * StreamClusters, which have app data. * The generated StreamBundle also has a url for next StreamBundle to be generated * with fresh app data. * Hence we loop as long as the StreamBundle's next page url is not blank. */ do { streamBundle = categoryHelper.getSubCategoryBundle(nextStreamBundleUrl) val streamClusters = streamBundle.streamClusters /* * Similarly to the logic of StreamBundles, each StreamCluster can have a url, * pointing to another StreamCluster with new set of app data. * We loop over all the StreamCluster of one StreamBundle, and for each of the * StreamCluster we continue looping as long as the StreamCluster.clusterNextPageUrl * is not blank. */ streamClusters.values.forEach { streamCluster -> list.addAll(streamCluster.clusterAppList) // Add all apps for this StreamCluster // Loop over possible next StreamClusters var currentStreamCluster = streamCluster while (currentStreamCluster.hasNext()) { currentStreamCluster = categoryHelper .getNextStreamCluster(currentStreamCluster.clusterNextPageUrl) .also { list.addAll(it.clusterAppList) } } } nextStreamBundleUrl = streamBundle.streamNextPageUrl } while (streamBundle.hasNext()) // TODO: DEAL WITH DUPLICATE AND LESS ITEMS /*val streamClusters = categoryHelper.getSubCategoryBundle(browseUrl).streamClusters streamClusters.values.forEach { list.addAll(it.clusterAppList) }*/ } } return list.distinctBy { it.packageName } Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +8 −0 Original line number Diff line number Diff line Loading @@ -82,4 +82,12 @@ class GPlayAPIRepository @Inject constructor( suspend fun listApps(browseUrl: String, authData: AuthData): List<App> { return gPlayAPIImpl.listApps(browseUrl, authData) } suspend fun listAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { return gPlayAPIImpl.listAppCategoryUrls(browseUrl, authData) } suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<App>, String> { return gPlayAPIImpl.getAppsAndNextClusterUrl(browseUrl, authData) } } app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +15 −0 Original line number Diff line number Diff line Loading @@ -163,6 +163,21 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu authData, args.source ) if (args.source != "Open Source" && args.source != "PWA") { /* * For Play store apps we try to load more apps on reaching end of list. * Source: https://stackoverflow.com/a/46342525 */ recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (!recyclerView.canScrollVertically(1)) { viewModel.getPlayStoreAppsOnScroll(args.browseUrl, authData) } } }) } } } } Loading Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +10 −0 Original line number Diff line number Diff line Loading @@ -227,6 +227,16 @@ class FusedAPIImpl @Inject constructor( } } suspend fun getPlayStoreAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { return gPlayAPIRepository.listAppCategoryUrls(browseUrl, authData) } suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<FusedApp>, String> { return gPlayAPIRepository.getAppsAndNextClusterUrl(browseUrl, authData).let { Pair(it.first.map { app -> app.transformToFusedApp() }, it.second) } } suspend fun getApplicationDetails( packageNameList: List<String>, authData: AuthData, Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +8 −0 Original line number Diff line number Diff line Loading @@ -94,6 +94,14 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.listApps(category, browseUrl, authData) } suspend fun getPlayStoreAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { return fusedAPIImpl.getPlayStoreAppCategoryUrls(browseUrl, authData) } suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<FusedApp>, String> { return fusedAPIImpl.getAppsAndNextClusterUrl(browseUrl, authData) } suspend fun getAppsListBasedOnCategory( category: String, browseUrl: String, Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +131 −11 Original line number Diff line number Diff line Loading @@ -19,21 +19,20 @@ package foundation.e.apps.api.gplay 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.Category import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.* import com.aurora.gplayapi.helpers.AppDetailsHelper import com.aurora.gplayapi.helpers.AuthValidator import com.aurora.gplayapi.helpers.CategoryHelper import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.StreamHelper import com.aurora.gplayapi.helpers.TopChartsHelper import foundation.e.apps.api.gplay.token.TokenRepository import foundation.e.apps.api.gplay.utils.GPlayHttpClient import foundation.e.apps.utils.modules.DataStoreModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import javax.inject.Inject Loading Loading @@ -80,9 +79,18 @@ class GPlayAPIImpl @Inject constructor( // Fetch more results in case the given result is a promoted app if (searchData.size == 1) { val searchBundle = searchHelper.next(searchResult.subBundles) val bundleSet: MutableSet<SearchBundle.SubBundle> = searchResult.subBundles do { val searchBundle = searchHelper.next(bundleSet) if (searchBundle.appList.isNotEmpty()) { searchData.addAll(searchBundle.appList) } bundleSet.apply { clear() addAll(searchBundle.subBundles) } } while (bundleSet.isNotEmpty()) } } return searchData } Loading Loading @@ -141,14 +149,126 @@ class GPlayAPIImpl @Inject constructor( return categoryList } /** * Get list of "clusterBrowseUrl" which can be used to get [StreamCluster] objects which * have "clusterNextPageUrl" to get subsequent [StreamCluster] objects. * * * -- browseUrl * | * StreamBundle 1 (streamNextPageUrl points to StreamBundle 2) * clusterBrowseUrl 1 -> clusterNextPageUrl 1.1 -> clusterNextPageUrl -> 1.2 .... * clusterBrowseUrl 2 -> clusterNextPageUrl 2.1 -> clusterNextPageUrl -> 2.2 .... * clusterBrowseUrl 3 -> clusterNextPageUrl 3.1 -> clusterNextPageUrl -> 3.2 .... * StreamBundle 2 * clusterBroseUrl 4 -> ... * clusterBroseUrl 5 -> ... * * This function returns the clusterBrowseUrls 1,2,3,4,5... */ suspend fun listAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { val urlList = mutableListOf<String>() withContext(Dispatchers.IO) { supervisorScope { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) var streamBundle: StreamBundle var nextStreamBundleUrl = browseUrl do { streamBundle = categoryHelper.getSubCategoryBundle(nextStreamBundleUrl) val streamClusters = streamBundle.streamClusters.values urlList.addAll(streamClusters.map { it.clusterBrowseUrl }) nextStreamBundleUrl = streamBundle.streamNextPageUrl } while (nextStreamBundleUrl.isNotBlank()) } } return urlList.distinct().filter { it.isNotBlank() } } /** * Accept a [browseUrl] of type "clusterBrowseUrl" or "clusterNextPageUrl". * Fetch a StreamCluster from the [browseUrl] and return pair of: * - List od apps to display. * - String url "clusterNextPageUrl" pointing to next StreamCluster. This can be blank (not null). */ suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<App>, String> { val streamCluster: StreamCluster withContext(Dispatchers.IO) { supervisorScope { val streamHelper = StreamHelper(authData).using(gPlayHttpClient) val browseResponse = streamHelper.getBrowseStreamResponse(browseUrl) streamCluster = if (browseResponse.contentsUrl.isNotEmpty()) { streamHelper.getNextStreamCluster(browseResponse.contentsUrl) } else if (browseResponse.hasBrowseTab()) { streamHelper.getNextStreamCluster(browseResponse.browseTab.listUrl) } else { StreamCluster() } } } return Pair(streamCluster.clusterAppList, streamCluster.clusterNextPageUrl) } suspend fun listApps(browseUrl: String, authData: AuthData): List<App> { val list = mutableListOf<App>() withContext(Dispatchers.IO) { supervisorScope { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) val streamClusters = categoryHelper.getSubCategoryBundle(browseUrl).streamClusters var streamBundle: StreamBundle var nextStreamBundleUrl = browseUrl /* * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 * * Logic: We start with the browseUrl. * When we call getSubCategoryBundle(), we get a new StreamBundle object, having * StreamClusters, which have app data. * The generated StreamBundle also has a url for next StreamBundle to be generated * with fresh app data. * Hence we loop as long as the StreamBundle's next page url is not blank. */ do { streamBundle = categoryHelper.getSubCategoryBundle(nextStreamBundleUrl) val streamClusters = streamBundle.streamClusters /* * Similarly to the logic of StreamBundles, each StreamCluster can have a url, * pointing to another StreamCluster with new set of app data. * We loop over all the StreamCluster of one StreamBundle, and for each of the * StreamCluster we continue looping as long as the StreamCluster.clusterNextPageUrl * is not blank. */ streamClusters.values.forEach { streamCluster -> list.addAll(streamCluster.clusterAppList) // Add all apps for this StreamCluster // Loop over possible next StreamClusters var currentStreamCluster = streamCluster while (currentStreamCluster.hasNext()) { currentStreamCluster = categoryHelper .getNextStreamCluster(currentStreamCluster.clusterNextPageUrl) .also { list.addAll(it.clusterAppList) } } } nextStreamBundleUrl = streamBundle.streamNextPageUrl } while (streamBundle.hasNext()) // TODO: DEAL WITH DUPLICATE AND LESS ITEMS /*val streamClusters = categoryHelper.getSubCategoryBundle(browseUrl).streamClusters streamClusters.values.forEach { list.addAll(it.clusterAppList) }*/ } } return list.distinctBy { it.packageName } Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +8 −0 Original line number Diff line number Diff line Loading @@ -82,4 +82,12 @@ class GPlayAPIRepository @Inject constructor( suspend fun listApps(browseUrl: String, authData: AuthData): List<App> { return gPlayAPIImpl.listApps(browseUrl, authData) } suspend fun listAppCategoryUrls(browseUrl: String, authData: AuthData): List<String> { return gPlayAPIImpl.listAppCategoryUrls(browseUrl, authData) } suspend fun getAppsAndNextClusterUrl(browseUrl: String, authData: AuthData): Pair<List<App>, String> { return gPlayAPIImpl.getAppsAndNextClusterUrl(browseUrl, authData) } }
app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +15 −0 Original line number Diff line number Diff line Loading @@ -163,6 +163,21 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu authData, args.source ) if (args.source != "Open Source" && args.source != "PWA") { /* * For Play store apps we try to load more apps on reaching end of list. * Source: https://stackoverflow.com/a/46342525 */ recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (!recyclerView.canScrollVertically(1)) { viewModel.getPlayStoreAppsOnScroll(args.browseUrl, authData) } } }) } } } } Loading