Loading app/src/main/java/foundation/e/apps/api/StoreRepository.kt 0 → 100644 +25 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api interface StoreRepository { suspend fun getHomeScreenData(): Any suspend fun getSearchResult(query: String): Any suspend fun getSearchSuggestions(query: String): Any } app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppsRepository.kt 0 → 100644 +51 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api.cleanapk import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.cleanapk.data.home.HomeScreen import foundation.e.apps.api.cleanapk.data.search.Search import retrofit2.Response class CleanApkAppsRepository( private val cleanAPKInterface: CleanAPKInterface, ) : StoreRepository { override suspend fun getHomeScreenData(): Response<HomeScreen> { return cleanAPKInterface.getHomeScreenData( CleanAPKInterface.APP_TYPE_ANY, CleanAPKInterface.APP_SOURCE_FOSS ) } override suspend fun getSearchResult(query: String): Response<Search> { return cleanAPKInterface.searchApps( query, CleanAPKInterface.APP_SOURCE_FOSS, CleanAPKInterface.APP_TYPE_ANY, 20, 1, null ) } override suspend fun getSearchSuggestions(query: String): Any { TODO("Not yet implemented") } } app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkPWARepository.kt 0 → 100644 +50 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api.cleanapk import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.cleanapk.data.search.Search import retrofit2.Response class CleanApkPWARepository( private val cleanAPKInterface: CleanAPKInterface, ) : StoreRepository { override suspend fun getHomeScreenData(): Any { return cleanAPKInterface.getHomeScreenData( CleanAPKInterface.APP_TYPE_PWA, CleanAPKInterface.APP_SOURCE_ANY ) } override suspend fun getSearchResult(query: String): Response<Search> { return cleanAPKInterface.searchApps( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA, 20, 1, null ) } override suspend fun getSearchSuggestions(query: String): Any { TODO("Not yet implemented") } } app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +59 −48 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData import androidx.lifecycle.LiveDataScope import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants Loading @@ -32,14 +33,15 @@ 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 com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.cleanapk.CleanAPKInterface import foundation.e.apps.api.cleanapk.CleanAPKRepository import foundation.e.apps.api.cleanapk.data.categories.Categories import foundation.e.apps.api.cleanapk.data.home.Home import foundation.e.apps.api.cleanapk.data.home.HomeScreen import foundation.e.apps.api.cleanapk.data.search.Search import foundation.e.apps.api.fdroid.FdroidWebInterface import foundation.e.apps.api.fused.data.FusedApp Loading @@ -63,11 +65,17 @@ import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PreferenceManagerModule import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withTimeout import retrofit2.Response import timber.log.Timber import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton typealias GplaySearchResultFlow = Flow<ResultSupreme<Pair<List<FusedApp>, Boolean>>> @Singleton class FusedAPIImpl @Inject constructor( private val cleanAPKRepository: CleanAPKRepository, Loading @@ -76,6 +84,9 @@ class FusedAPIImpl @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val preferenceManagerModule: PreferenceManagerModule, private val fdroidWebInterface: FdroidWebInterface, @Named("gplayRepository") private val gplayRepository: StoreRepository, @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: StoreRepository, @Named("cleanApkPWARepository") private val cleanApkPWARepository: StoreRepository, @ApplicationContext private val context: Context ) { Loading Loading @@ -152,20 +163,16 @@ class FusedAPIImpl @Inject constructor( }) Source.OPEN -> runCodeBlockWithTimeout({ val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_ANY, CleanAPKInterface.APP_SOURCE_FOSS ).body() val response = (cleanApkAppsRepository.getHomeScreenData() as Response<HomeScreen>).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } }) Source.PWA -> runCodeBlockWithTimeout({ val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_PWA, CleanAPKInterface.APP_SOURCE_ANY ).body() val response = (cleanApkPWARepository.getHomeScreenData() as Response<HomeScreen>).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_PWA)) } Loading Loading @@ -272,7 +279,7 @@ class FusedAPIImpl @Inject constructor( authData, searchResult, packageSpecificResults ) ).asLiveData() ) } } Loading @@ -286,11 +293,9 @@ class FusedAPIImpl @Inject constructor( ): ResultSupreme<Pair<List<FusedApp>, Boolean>> { val pwaApps: MutableList<FusedApp> = mutableListOf() val status = fusedAPIImpl.runCodeBlockWithTimeout({ getCleanAPKSearchResults( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA ).apply { val apps = (cleanApkPWARepository.getSearchResult(query) as Response<Search>).body()?.apps apps?.apply { if (this.isNotEmpty()) { pwaApps.addAll(this) } Loading @@ -314,13 +319,12 @@ class FusedAPIImpl @Inject constructor( ) } private fun fetchGplaySearchResults( private suspend fun fetchGplaySearchResults( query: String, authData: AuthData, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> = getGplaySearchResults(query, authData).map { ): GplaySearchResultFlow = getGplaySearchResult(query, authData).map { if (it.first.isNotEmpty()) { searchResult.addAll(it.first) } Loading Loading @@ -469,7 +473,7 @@ class FusedAPIImpl @Inject constructor( } suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> { return gPlayAPIRepository.getSearchSuggestions(query, authData) return gplayRepository.getSearchSuggestions(query) as List<SearchSuggestEntry> } suspend fun getOnDemandModule( Loading Loading @@ -1174,7 +1178,7 @@ class FusedAPIImpl @Inject constructor( ): List<FusedApp> { val list = mutableListOf<FusedApp>() val response = cleanAPKRepository.searchApps(keyword, source, type, nres, page, by).body()?.apps (cleanApkAppsRepository.getSearchResult(keyword) as Response<Search>).body()?.apps response?.forEach { it.updateStatus() Loading @@ -1190,7 +1194,8 @@ class FusedAPIImpl @Inject constructor( query: String, authData: AuthData ): LiveData<Pair<List<FusedApp>, Boolean>> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid) val searchResults = gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid) return searchResults.map { Pair( it.first, Loading @@ -1199,6 +1204,20 @@ class FusedAPIImpl @Inject constructor( } } private suspend fun getGplaySearchResult( query: String, authData: AuthData ): Flow<Pair<List<FusedApp>, Boolean>> { val searchResults = gplayRepository.getSearchResult(query) as Flow<Pair<List<App>, Boolean>> return searchResults.map { val fusedAppList = it.first.map { app -> replaceWithFDroid(app) } Pair( fusedAppList, it.second ) } } /* * This function will replace a GPlay app with F-Droid app if exists, * else will show the GPlay app itself. Loading Loading @@ -1320,24 +1339,16 @@ class FusedAPIImpl @Inject constructor( private suspend fun fetchGPlayHome(authData: AuthData): List<FusedHome> { val list = mutableListOf<FusedHome>() val homeElements = mutableMapOf( context.getString(R.string.topselling_free_apps) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topselling_free_games) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME), context.getString(R.string.topgrossing_apps) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topgrossing_games) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.GAME), context.getString(R.string.movers_shakers_apps) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION), context.getString(R.string.movers_shakers_games) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), ) homeElements.forEach { val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() val result = gPlayAPIRepository.getTopApps(type, chart, authData).map { app -> val gplayHomeData = gplayRepository.getHomeScreenData() as Map<String, List<App>> gplayHomeData.map { val fusedApps = it.value.map { app -> app.transformToFusedApp().apply { updateFilterLevel(authData) } } list.add(FusedHome(it.key, result)) list.add(FusedHome(it.key, fusedApps)) } Timber.d("===> $list") return list } Loading app/src/main/java/foundation/e/apps/api/gplay/GplayRepository.kt 0 → 100644 +206 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api.gplay import android.content.Context 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.SearchBundle import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.gplay.utils.GPlayHttpClient import foundation.e.apps.login.LoginSourceRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import javax.inject.Inject class GplayRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, private val loginSourceRepository: LoginSourceRepository ) : StoreRepository { override suspend fun getHomeScreenData(): Any { val homeScreenData = mutableMapOf<String, List<App>>() val homeElements = createTopChartElements() homeElements.forEach { val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() val result = getTopApps(type, chart, loginSourceRepository.gplayAuth!!) homeScreenData[it.key] = result } return homeScreenData } private fun createTopChartElements() = mutableMapOf( context.getString(R.string.topselling_free_apps) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topselling_free_games) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME), context.getString(R.string.topgrossing_apps) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topgrossing_games) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.GAME), context.getString(R.string.movers_shakers_apps) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION), context.getString(R.string.movers_shakers_games) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), ) override suspend fun getSearchResult(query: String): Flow<Pair<List<App>, Boolean>> { return flow { /* * Variable names and logic made same as that of Aurora store. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ val searchHelper = SearchHelper(loginSourceRepository.gplayAuth!!).using(gPlayHttpClient) val searchBundle = searchHelper.searchResults(query) val initialReplacedList = mutableListOf<App>() val INITIAL_LIMIT = 4 emitReplacedList( this@flow, initialReplacedList, INITIAL_LIMIT, searchBundle, true, ) var nextSubBundleSet: MutableSet<SearchBundle.SubBundle> do { nextSubBundleSet = fetchNextSubBundle( searchBundle, searchHelper, this@flow, initialReplacedList, INITIAL_LIMIT ) } while (nextSubBundleSet.isNotEmpty()) /* * If initialReplacedList size is less than INITIAL_LIMIT, * it means the results were very less and nothing has been emitted so far. * Hence emit the list. */ if (initialReplacedList.size < INITIAL_LIMIT) { emitInMain(this@flow, initialReplacedList, false) } }.flowOn(Dispatchers.IO) } private suspend fun fetchNextSubBundle( searchBundle: SearchBundle, searchHelper: SearchHelper, scope: FlowCollector<Pair<List<App>, Boolean>>, accumulationList: MutableList<App>, accumulationLimit: Int, ): MutableSet<SearchBundle.SubBundle> { val nextSubBundleSet = searchBundle.subBundles val newSearchBundle = searchHelper.next(nextSubBundleSet) if (newSearchBundle.appList.isNotEmpty()) { searchBundle.apply { subBundles.clear() subBundles.addAll(newSearchBundle.subBundles) emitReplacedList( scope, accumulationList, accumulationLimit, newSearchBundle, nextSubBundleSet.isNotEmpty(), ) } } return nextSubBundleSet } override suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry> { val searchData = mutableListOf<SearchSuggestEntry>() withContext(Dispatchers.IO) { val searchHelper = SearchHelper(loginSourceRepository.gplayAuth!!).using(gPlayHttpClient) searchData.addAll(searchHelper.searchSuggestions(query)) } return searchData.filter { it.suggestedQuery.isNotBlank() } } private suspend fun emitReplacedList( scope: FlowCollector<Pair<List<App>, Boolean>>, accumulationList: MutableList<App>, accumulationLimit: Int, searchBundle: SearchBundle, moreToEmit: Boolean, ) { searchBundle.appList.forEach { when { accumulationList.size < accumulationLimit - 1 -> { /* * If initial limit is 4, add apps to list (without emitting) * till 2 apps. */ accumulationList.add(it) } accumulationList.size == accumulationLimit - 1 -> { /* * If initial limit is 4, and we have reached till 3 apps, * add the 4th app and emit the list. */ accumulationList.add(it) scope.emit(Pair(accumulationList, moreToEmit)) emitInMain(scope, accumulationList, moreToEmit) } accumulationList.size == accumulationLimit -> { /* * If initial limit is 4, and we have emitted 4 apps, * for all rest of the apps, emit each app one by one. */ emitInMain(scope, listOf(it), moreToEmit) } } } } private suspend fun emitInMain( scope: FlowCollector<Pair<List<App>, Boolean>>, it: List<App>, moreToEmit: Boolean ) { scope.emit(Pair(it, moreToEmit)) } private suspend fun getTopApps( type: TopChartsHelper.Type, chart: TopChartsHelper.Chart, authData: AuthData ): List<App> { val topApps = mutableListOf<App>() withContext(Dispatchers.IO) { val topChartsHelper = TopChartsHelper(authData).using(gPlayHttpClient) topApps.addAll(topChartsHelper.getCluster(type, chart).clusterAppList) } return topApps } } Loading
app/src/main/java/foundation/e/apps/api/StoreRepository.kt 0 → 100644 +25 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api interface StoreRepository { suspend fun getHomeScreenData(): Any suspend fun getSearchResult(query: String): Any suspend fun getSearchSuggestions(query: String): Any }
app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppsRepository.kt 0 → 100644 +51 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api.cleanapk import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.cleanapk.data.home.HomeScreen import foundation.e.apps.api.cleanapk.data.search.Search import retrofit2.Response class CleanApkAppsRepository( private val cleanAPKInterface: CleanAPKInterface, ) : StoreRepository { override suspend fun getHomeScreenData(): Response<HomeScreen> { return cleanAPKInterface.getHomeScreenData( CleanAPKInterface.APP_TYPE_ANY, CleanAPKInterface.APP_SOURCE_FOSS ) } override suspend fun getSearchResult(query: String): Response<Search> { return cleanAPKInterface.searchApps( query, CleanAPKInterface.APP_SOURCE_FOSS, CleanAPKInterface.APP_TYPE_ANY, 20, 1, null ) } override suspend fun getSearchSuggestions(query: String): Any { TODO("Not yet implemented") } }
app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkPWARepository.kt 0 → 100644 +50 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api.cleanapk import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.cleanapk.data.search.Search import retrofit2.Response class CleanApkPWARepository( private val cleanAPKInterface: CleanAPKInterface, ) : StoreRepository { override suspend fun getHomeScreenData(): Any { return cleanAPKInterface.getHomeScreenData( CleanAPKInterface.APP_TYPE_PWA, CleanAPKInterface.APP_SOURCE_ANY ) } override suspend fun getSearchResult(query: String): Response<Search> { return cleanAPKInterface.searchApps( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA, 20, 1, null ) } override suspend fun getSearchSuggestions(query: String): Any { TODO("Not yet implemented") } }
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +59 −48 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData import androidx.lifecycle.LiveDataScope import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants Loading @@ -32,14 +33,15 @@ 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 com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.cleanapk.CleanAPKInterface import foundation.e.apps.api.cleanapk.CleanAPKRepository import foundation.e.apps.api.cleanapk.data.categories.Categories import foundation.e.apps.api.cleanapk.data.home.Home import foundation.e.apps.api.cleanapk.data.home.HomeScreen import foundation.e.apps.api.cleanapk.data.search.Search import foundation.e.apps.api.fdroid.FdroidWebInterface import foundation.e.apps.api.fused.data.FusedApp Loading @@ -63,11 +65,17 @@ import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.PWAManagerModule import foundation.e.apps.utils.modules.PreferenceManagerModule import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withTimeout import retrofit2.Response import timber.log.Timber import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton typealias GplaySearchResultFlow = Flow<ResultSupreme<Pair<List<FusedApp>, Boolean>>> @Singleton class FusedAPIImpl @Inject constructor( private val cleanAPKRepository: CleanAPKRepository, Loading @@ -76,6 +84,9 @@ class FusedAPIImpl @Inject constructor( private val pwaManagerModule: PWAManagerModule, private val preferenceManagerModule: PreferenceManagerModule, private val fdroidWebInterface: FdroidWebInterface, @Named("gplayRepository") private val gplayRepository: StoreRepository, @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: StoreRepository, @Named("cleanApkPWARepository") private val cleanApkPWARepository: StoreRepository, @ApplicationContext private val context: Context ) { Loading Loading @@ -152,20 +163,16 @@ class FusedAPIImpl @Inject constructor( }) Source.OPEN -> runCodeBlockWithTimeout({ val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_ANY, CleanAPKInterface.APP_SOURCE_FOSS ).body() val response = (cleanApkAppsRepository.getHomeScreenData() as Response<HomeScreen>).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } }) Source.PWA -> runCodeBlockWithTimeout({ val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_PWA, CleanAPKInterface.APP_SOURCE_ANY ).body() val response = (cleanApkPWARepository.getHomeScreenData() as Response<HomeScreen>).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_PWA)) } Loading Loading @@ -272,7 +279,7 @@ class FusedAPIImpl @Inject constructor( authData, searchResult, packageSpecificResults ) ).asLiveData() ) } } Loading @@ -286,11 +293,9 @@ class FusedAPIImpl @Inject constructor( ): ResultSupreme<Pair<List<FusedApp>, Boolean>> { val pwaApps: MutableList<FusedApp> = mutableListOf() val status = fusedAPIImpl.runCodeBlockWithTimeout({ getCleanAPKSearchResults( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA ).apply { val apps = (cleanApkPWARepository.getSearchResult(query) as Response<Search>).body()?.apps apps?.apply { if (this.isNotEmpty()) { pwaApps.addAll(this) } Loading @@ -314,13 +319,12 @@ class FusedAPIImpl @Inject constructor( ) } private fun fetchGplaySearchResults( private suspend fun fetchGplaySearchResults( query: String, authData: AuthData, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> = getGplaySearchResults(query, authData).map { ): GplaySearchResultFlow = getGplaySearchResult(query, authData).map { if (it.first.isNotEmpty()) { searchResult.addAll(it.first) } Loading Loading @@ -469,7 +473,7 @@ class FusedAPIImpl @Inject constructor( } suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> { return gPlayAPIRepository.getSearchSuggestions(query, authData) return gplayRepository.getSearchSuggestions(query) as List<SearchSuggestEntry> } suspend fun getOnDemandModule( Loading Loading @@ -1174,7 +1178,7 @@ class FusedAPIImpl @Inject constructor( ): List<FusedApp> { val list = mutableListOf<FusedApp>() val response = cleanAPKRepository.searchApps(keyword, source, type, nres, page, by).body()?.apps (cleanApkAppsRepository.getSearchResult(keyword) as Response<Search>).body()?.apps response?.forEach { it.updateStatus() Loading @@ -1190,7 +1194,8 @@ class FusedAPIImpl @Inject constructor( query: String, authData: AuthData ): LiveData<Pair<List<FusedApp>, Boolean>> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid) val searchResults = gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid) return searchResults.map { Pair( it.first, Loading @@ -1199,6 +1204,20 @@ class FusedAPIImpl @Inject constructor( } } private suspend fun getGplaySearchResult( query: String, authData: AuthData ): Flow<Pair<List<FusedApp>, Boolean>> { val searchResults = gplayRepository.getSearchResult(query) as Flow<Pair<List<App>, Boolean>> return searchResults.map { val fusedAppList = it.first.map { app -> replaceWithFDroid(app) } Pair( fusedAppList, it.second ) } } /* * This function will replace a GPlay app with F-Droid app if exists, * else will show the GPlay app itself. Loading Loading @@ -1320,24 +1339,16 @@ class FusedAPIImpl @Inject constructor( private suspend fun fetchGPlayHome(authData: AuthData): List<FusedHome> { val list = mutableListOf<FusedHome>() val homeElements = mutableMapOf( context.getString(R.string.topselling_free_apps) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topselling_free_games) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME), context.getString(R.string.topgrossing_apps) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topgrossing_games) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.GAME), context.getString(R.string.movers_shakers_apps) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION), context.getString(R.string.movers_shakers_games) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), ) homeElements.forEach { val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() val result = gPlayAPIRepository.getTopApps(type, chart, authData).map { app -> val gplayHomeData = gplayRepository.getHomeScreenData() as Map<String, List<App>> gplayHomeData.map { val fusedApps = it.value.map { app -> app.transformToFusedApp().apply { updateFilterLevel(authData) } } list.add(FusedHome(it.key, result)) list.add(FusedHome(it.key, fusedApps)) } Timber.d("===> $list") return list } Loading
app/src/main/java/foundation/e/apps/api/gplay/GplayRepository.kt 0 → 100644 +206 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * Apps Quickly and easily install Android apps onto your device! * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.apps.api.gplay import android.content.Context 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.SearchBundle import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.TopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.api.StoreRepository import foundation.e.apps.api.gplay.utils.GPlayHttpClient import foundation.e.apps.login.LoginSourceRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext import javax.inject.Inject class GplayRepository @Inject constructor( @ApplicationContext private val context: Context, private val gPlayHttpClient: GPlayHttpClient, private val loginSourceRepository: LoginSourceRepository ) : StoreRepository { override suspend fun getHomeScreenData(): Any { val homeScreenData = mutableMapOf<String, List<App>>() val homeElements = createTopChartElements() homeElements.forEach { val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() val result = getTopApps(type, chart, loginSourceRepository.gplayAuth!!) homeScreenData[it.key] = result } return homeScreenData } private fun createTopChartElements() = mutableMapOf( context.getString(R.string.topselling_free_apps) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topselling_free_games) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME), context.getString(R.string.topgrossing_apps) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION), context.getString(R.string.topgrossing_games) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.GAME), context.getString(R.string.movers_shakers_apps) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION), context.getString(R.string.movers_shakers_games) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), ) override suspend fun getSearchResult(query: String): Flow<Pair<List<App>, Boolean>> { return flow { /* * Variable names and logic made same as that of Aurora store. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ val searchHelper = SearchHelper(loginSourceRepository.gplayAuth!!).using(gPlayHttpClient) val searchBundle = searchHelper.searchResults(query) val initialReplacedList = mutableListOf<App>() val INITIAL_LIMIT = 4 emitReplacedList( this@flow, initialReplacedList, INITIAL_LIMIT, searchBundle, true, ) var nextSubBundleSet: MutableSet<SearchBundle.SubBundle> do { nextSubBundleSet = fetchNextSubBundle( searchBundle, searchHelper, this@flow, initialReplacedList, INITIAL_LIMIT ) } while (nextSubBundleSet.isNotEmpty()) /* * If initialReplacedList size is less than INITIAL_LIMIT, * it means the results were very less and nothing has been emitted so far. * Hence emit the list. */ if (initialReplacedList.size < INITIAL_LIMIT) { emitInMain(this@flow, initialReplacedList, false) } }.flowOn(Dispatchers.IO) } private suspend fun fetchNextSubBundle( searchBundle: SearchBundle, searchHelper: SearchHelper, scope: FlowCollector<Pair<List<App>, Boolean>>, accumulationList: MutableList<App>, accumulationLimit: Int, ): MutableSet<SearchBundle.SubBundle> { val nextSubBundleSet = searchBundle.subBundles val newSearchBundle = searchHelper.next(nextSubBundleSet) if (newSearchBundle.appList.isNotEmpty()) { searchBundle.apply { subBundles.clear() subBundles.addAll(newSearchBundle.subBundles) emitReplacedList( scope, accumulationList, accumulationLimit, newSearchBundle, nextSubBundleSet.isNotEmpty(), ) } } return nextSubBundleSet } override suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry> { val searchData = mutableListOf<SearchSuggestEntry>() withContext(Dispatchers.IO) { val searchHelper = SearchHelper(loginSourceRepository.gplayAuth!!).using(gPlayHttpClient) searchData.addAll(searchHelper.searchSuggestions(query)) } return searchData.filter { it.suggestedQuery.isNotBlank() } } private suspend fun emitReplacedList( scope: FlowCollector<Pair<List<App>, Boolean>>, accumulationList: MutableList<App>, accumulationLimit: Int, searchBundle: SearchBundle, moreToEmit: Boolean, ) { searchBundle.appList.forEach { when { accumulationList.size < accumulationLimit - 1 -> { /* * If initial limit is 4, add apps to list (without emitting) * till 2 apps. */ accumulationList.add(it) } accumulationList.size == accumulationLimit - 1 -> { /* * If initial limit is 4, and we have reached till 3 apps, * add the 4th app and emit the list. */ accumulationList.add(it) scope.emit(Pair(accumulationList, moreToEmit)) emitInMain(scope, accumulationList, moreToEmit) } accumulationList.size == accumulationLimit -> { /* * If initial limit is 4, and we have emitted 4 apps, * for all rest of the apps, emit each app one by one. */ emitInMain(scope, listOf(it), moreToEmit) } } } } private suspend fun emitInMain( scope: FlowCollector<Pair<List<App>, Boolean>>, it: List<App>, moreToEmit: Boolean ) { scope.emit(Pair(it, moreToEmit)) } private suspend fun getTopApps( type: TopChartsHelper.Type, chart: TopChartsHelper.Chart, authData: AuthData ): List<App> { val topApps = mutableListOf<App>() withContext(Dispatchers.IO) { val topChartsHelper = TopChartsHelper(authData).using(gPlayHttpClient) topApps.addAll(topChartsHelper.getCluster(type, chart).clusterAppList) } return topApps } }