Loading app/build.gradle +1 −2 Original line number Diff line number Diff line Loading @@ -135,8 +135,7 @@ dependencies { // Optional -- mockito-kotlin testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0" testImplementation 'org.mockito:mockito-inline:2.13.0' testImplementation "androidx.arch.core:core-testing:2.1.0" // Coil and PhotoView implementation "io.coil-kt:coil:1.4.0" Loading app/src/main/java/foundation/e/apps/api/DownloadManager.kt +0 −1 Original line number Diff line number Diff line Loading @@ -125,7 +125,6 @@ class DownloadManager @Inject constructor( } catch (e: Exception) { Timber.e(e) } } private fun tickerFlow(downloadId: Long, period: Duration, initialDelay: Duration = Duration.ZERO) = flow { Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +296 −195 Original line number Diff line number Diff line Loading @@ -144,23 +144,28 @@ class FusedAPIImpl @Inject constructor( * To prevent waiting so long and fail early, use withTimeout{}. */ withTimeout(timeoutDurationInMillis) { if (applicationType != APP_TYPE_ANY) { val response = if (applicationType == APP_TYPE_OPEN) { cleanAPKRepository.getHomeScreenData( if (preferenceManagerModule.isGplaySelected()) { list.addAll(fetchGPlayHome(authData)) } if (preferenceManagerModule.isOpenSourceSelected()) { val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_ANY, CleanAPKInterface.APP_SOURCE_FOSS ).body() } else { cleanAPKRepository.getHomeScreenData( response?.home?.let { list.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } } if (preferenceManagerModule.isPWASelected()) { val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_PWA, CleanAPKInterface.APP_SOURCE_ANY ).body() } response?.home?.let { list.addAll(generateCleanAPKHome(it, applicationType)) list.addAll(generateCleanAPKHome(it, APP_TYPE_PWA)) } } else { list.addAll(fetchGPlayHome(authData)) } } } catch (e: TimeoutCancellationException) { Loading Loading @@ -194,20 +199,12 @@ class FusedAPIImpl @Inject constructor( var apiStatus: ResultStatus = ResultStatus.OK var applicationCategoryType = preferredApplicationType if (preferredApplicationType != APP_TYPE_ANY) { handleCleanApkCategories(preferredApplicationType, categoriesList, type).run { if (this != ResultStatus.OK) { apiStatus = this } } } else { handleAllSourcesCategories(categoriesList, type, authData).run { if (first != ResultStatus.OK) { apiStatus = first applicationCategoryType = second } } } categoriesList.sortBy { item -> item.title.lowercase() } return Triple(categoriesList, applicationCategoryType, apiStatus) } Loading @@ -231,33 +228,149 @@ class FusedAPIImpl @Inject constructor( */ return liveData { val packageSpecificResults = ArrayList<FusedApp>() var gplayPackageResult: FusedApp? = null var cleanapkPackageResult: FusedApp? = null fetchPackageSpecificResult(authData, query, packageSpecificResults)?.let { if (it.data?.second == true) { // if there are no data to load emit(it) return@liveData } } val status = runCodeBlockWithTimeout({ if (preferenceManagerModule.preferredApplicationType() == APP_TYPE_ANY) { try { /* * Surrounding with try-catch because if query is not a package name, * then GPlay throws an error. */ getApplicationDetails(query, query, authData, Origin.GPLAY).let { if (it.second == ResultStatus.OK) { gplayPackageResult = it.first val searchResult = mutableListOf<FusedApp>() val cleanApkResults = mutableListOf<FusedApp>() if (preferenceManagerModule.isOpenSourceSelected()) { fetchOpenSourceSearchResult( this@FusedAPIImpl, cleanApkResults, query, searchResult, packageSpecificResults )?.let { emit(it) } } if (preferenceManagerModule.isGplaySelected()) { emitSource( fetchGplaySearchResults( query, authData, searchResult, packageSpecificResults ) ) } } catch (e: Exception) { Timber.e(e) if (preferenceManagerModule.isPWASelected()) { fetchPWASearchResult( this@FusedAPIImpl, query, searchResult, packageSpecificResults )?.let { emit(it) } } } getCleanapkSearchResult(query).let { /* Cleanapk always returns something, it is never null. * If nothing is found, it returns a blank FusedApp() object. * Blank result to be filtered out. */ if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { cleanapkPackageResult = it.data!! } private suspend fun fetchPWASearchResult( fusedAPIImpl: FusedAPIImpl, query: String, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): 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 { if (this.isNotEmpty()) { pwaApps.addAll(this) } } }) if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { searchResult.addAll(pwaApps) return ResultSupreme.create( status, Pair( filterWithKeywordSearch( searchResult, packageSpecificResults, query ), false ) ) } return null } private fun fetchGplaySearchResults( query: String, authData: AuthData, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> = getGplaySearchResults(query, authData).map { if (it.first.isNotEmpty()) { searchResult.addAll(it.first) } ResultSupreme.Success( Pair( filterWithKeywordSearch( searchResult, packageSpecificResults, query ), it.second ) ) } private suspend fun fetchOpenSourceSearchResult( fusedAPIImpl: FusedAPIImpl, cleanApkResults: MutableList<FusedApp>, query: String, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? { val status = fusedAPIImpl.runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) { searchResult.addAll(cleanApkResults) return ResultSupreme.create( status, Pair( filterWithKeywordSearch( searchResult, packageSpecificResults, query ), preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected() ) ) } return null } private suspend fun fetchPackageSpecificResult( authData: AuthData, query: String, packageSpecificResults: MutableList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? { var gplayPackageResult: FusedApp? = null var cleanapkPackageResult: FusedApp? = null val status = runCodeBlockWithTimeout({ if (preferenceManagerModule.isGplaySelected()) { gplayPackageResult = getGplayPackagResult(query, authData) } if (preferenceManagerModule.isOpenSourceSelected()) { cleanapkPackageResult = getCleanApkPackageResult(query) } }) Loading @@ -275,8 +388,9 @@ class FusedAPIImpl @Inject constructor( * Also send true in the pair to signal more results being loaded. */ if (status != ResultStatus.OK) { emit(ResultSupreme.create(status, Pair(packageSpecificResults, true))) return@liveData return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } return ResultSupreme.create(status, Pair(packageSpecificResults, false)) } /* Loading @@ -287,81 +401,41 @@ class FusedAPIImpl @Inject constructor( * But for the other keyword related search results, we do not allow duplicate package names. * We also filter out apps which are already present in packageSpecificResults list. */ fun filterWithKeywordSearch(list: List<FusedApp>): List<FusedApp> { private fun filterWithKeywordSearch( list: List<FusedApp>, packageSpecificResults: List<FusedApp>, query: String ): List<FusedApp> { val filteredResults = list.distinctBy { it.package_name } .filter { packageSpecificResults.isEmpty() || it.package_name != query } return packageSpecificResults + filteredResults } val cleanApkResults = mutableListOf<FusedApp>() when (preferenceManagerModule.preferredApplicationType()) { APP_TYPE_ANY -> { val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) { /* * If cleanapk results are empty, dont emit emit data as it may * briefly show "No apps found..." * If status is timeout, then do emit the value. * Send true in the pair to signal more results (i.e from GPlay) being loaded. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), true) ) ) } emitSource( getGplayAndCleanapkCombinedResults(query, authData, cleanApkResults).map { /* * We are assuming that there will be no timeout here. * If there had to be any timeout, it would already have happened * while fetching package specific results. */ ResultSupreme.Success( Pair( filterWithKeywordSearch(it.first), it.second ) ) private suspend fun getCleanApkPackageResult( query: String, ): FusedApp? { getCleanapkSearchResult(query).let { if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { return it.data!! } ) } APP_TYPE_OPEN -> { val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) /* * Send false in pair to signal no more results to load, as only cleanapk * results are fetched, we don't have to wait for GPlay results. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), false) ) ) return null } APP_TYPE_PWA -> { val status = runCodeBlockWithTimeout({ cleanApkResults.addAll( getCleanAPKSearchResults( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA ) ) }) /* * Send false in pair to signal no more results to load, as only cleanapk * results are fetched for PWAs. */ emit(ResultSupreme.create(status, Pair(cleanApkResults, false))) private suspend fun getGplayPackagResult( query: String, authData: AuthData, ): FusedApp? { try { getApplicationDetails(query, query, authData, Origin.GPLAY).let { if (it.second == ResultStatus.OK) { return it.first } } } catch (e: Exception) { Timber.e(e) } return null } /* Loading Loading @@ -409,7 +483,13 @@ class FusedAPIImpl @Inject constructor( versionCode: Int, offerType: Int ): String? { val list = gPlayAPIRepository.getOnDemandModule(packageName, moduleName, versionCode, offerType, authData) val list = gPlayAPIRepository.getOnDemandModule( packageName, moduleName, versionCode, offerType, authData ) for (element in list) { if (element.name == "$moduleName.apk") { return element.url Loading @@ -418,7 +498,6 @@ class FusedAPIImpl @Inject constructor( return null } suspend fun updateFusedDownloadWithDownloadingInfo( authData: AuthData, origin: Origin, Loading Loading @@ -832,39 +911,69 @@ class FusedAPIImpl @Inject constructor( type: Category.Type, authData: AuthData ): Pair<ResultStatus, String> { var data: Categories? = null var apiStatus = ResultStatus.OK var errorApplicationCategory = "" /* * Try within timeout limit for open source native apps categories. */ runCodeBlockWithTimeout({ data = getOpenSourceCategories() data?.let { categoriesList.addAll( getFusedCategoryBasedOnCategoryType( it, if (preferenceManagerModule.isOpenSourceSelected()) { val openSourceCategoryResult = fetchOpenSourceCategories(type) categoriesList.addAll(openSourceCategoryResult.second) apiStatus = openSourceCategoryResult.first errorApplicationCategory = openSourceCategoryResult.third } if (preferenceManagerModule.isPWASelected()) { val pwaCategoriesResult = fetchPWACategories(type) categoriesList.addAll(pwaCategoriesResult.second) apiStatus = pwaCategoriesResult.first errorApplicationCategory = pwaCategoriesResult.third } if (preferenceManagerModule.isGplaySelected()) { val gplayCategoryResult = fetchGplayCategories( type, AppTag.OpenSource(context.getString(R.string.open_source)) ) authData ) categoriesList.addAll(gplayCategoryResult.second) apiStatus = gplayCategoryResult.first errorApplicationCategory = gplayCategoryResult.third } return Pair(apiStatus, errorApplicationCategory) } private suspend fun FusedAPIImpl.fetchGplayCategories( type: Category.Type, authData: AuthData, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus = ResultStatus.OK val categoryList = mutableListOf<FusedCategory>() runCodeBlockWithTimeout({ val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category, app) category } categoryList.addAll(playResponse) }, { errorApplicationCategory = APP_TYPE_OPEN errorApplicationCategory = APP_TYPE_ANY apiStatus = ResultStatus.TIMEOUT }, { errorApplicationCategory = APP_TYPE_OPEN errorApplicationCategory = APP_TYPE_ANY apiStatus = ResultStatus.UNKNOWN }) return Triple(apiStatus, categoryList, errorApplicationCategory) } /* * Try within timeout limit to get PWA categories */ private suspend fun FusedAPIImpl.fetchPWACategories( type: Category.Type, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoriesList = mutableListOf<FusedCategory>() runCodeBlockWithTimeout({ data = getPWAsCategories() data?.let { categoriesList.addAll( getPWAsCategories()?.let { fusedCategoriesList.addAll( getFusedCategoryBasedOnCategoryType( it, type, AppTag.PWA(context.getString(R.string.pwa)) ) Loading @@ -877,26 +986,33 @@ class FusedAPIImpl @Inject constructor( errorApplicationCategory = APP_TYPE_PWA apiStatus = ResultStatus.UNKNOWN }) return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) } /* * Try within timeout limit to get native app categories from Play Store */ private suspend fun FusedAPIImpl.fetchOpenSourceCategories( type: Category.Type, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoryList = mutableListOf<FusedCategory>() runCodeBlockWithTimeout({ val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category, app) category getOpenSourceCategories()?.let { fusedCategoryList.addAll( getFusedCategoryBasedOnCategoryType( it, type, AppTag.OpenSource(context.getString(R.string.open_source)) ) ) } categoriesList.addAll(playResponse) }, { errorApplicationCategory = APP_TYPE_ANY errorApplicationCategory = APP_TYPE_OPEN apiStatus = ResultStatus.TIMEOUT }, { errorApplicationCategory = APP_TYPE_ANY errorApplicationCategory = APP_TYPE_OPEN apiStatus = ResultStatus.UNKNOWN }) return Pair(apiStatus, errorApplicationCategory) return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) } /** Loading Loading @@ -1077,24 +1193,6 @@ class FusedAPIImpl @Inject constructor( return list } /* * Function to return a livedata with value from cleanapk and Google Play store combined. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ private fun getGplayAndCleanapkCombinedResults( query: String, authData: AuthData, cleanApkResults: List<FusedApp> ): LiveData<Pair<List<FusedApp>, Boolean>> { val localList = ArrayList<FusedApp>(cleanApkResults) return getGplaySearchResults(query, authData).map { pair -> Pair( localList.apply { addAll(pair.first) }.distinctBy { it.package_name }, pair.second ) } } private fun getGplaySearchResults( query: String, authData: AuthData Loading @@ -1112,9 +1210,9 @@ class FusedAPIImpl @Inject constructor( * Home screen-related internal functions */ private suspend fun generateCleanAPKHome(home: Home, prefType: String): List<FusedHome> { private suspend fun generateCleanAPKHome(home: Home, appType: String): List<FusedHome> { val list = mutableListOf<FusedHome>() val headings = if (prefType == APP_TYPE_OPEN) { val headings = if (appType == APP_TYPE_OPEN) { mapOf( "top_updated_apps" to context.getString(R.string.top_updated_apps), "top_updated_games" to context.getString(R.string.top_updated_games), Loading Loading @@ -1203,7 +1301,10 @@ class FusedAPIImpl @Inject constructor( } } } return list return list.map { it.source = appType it } } private suspend fun fetchGPlayHome(authData: AuthData): List<FusedHome> { Loading app/src/main/java/foundation/e/apps/api/fused/data/FusedHome.kt +2 −1 Original line number Diff line number Diff line Loading @@ -20,5 +20,6 @@ package foundation.e.apps.api.fused.data data class FusedHome( val title: String = String(), val list: List<FusedApp> = emptyList() val list: List<FusedApp> = emptyList(), var source: String = String() ) app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt +17 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package foundation.e.apps.home.model import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager Loading @@ -26,6 +27,8 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIImpl import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedHome Loading Loading @@ -70,6 +73,20 @@ class HomeParentRVAdapter( homeChildRVAdapter.setData(fusedHome.list) holder.binding.titleTV.text = fusedHome.title when (fusedHome.source) { FusedAPIImpl.APP_TYPE_OPEN -> { holder.binding.categoryTag.visibility = View.VISIBLE holder.binding.categoryTag.text = holder.binding.root.context.getString(R.string.open_source) } FusedAPIImpl.APP_TYPE_PWA -> { holder.binding.categoryTag.visibility = View.VISIBLE holder.binding.categoryTag.text = holder.binding.root.context.getString(R.string.pwa) } else -> { holder.binding.categoryTag.visibility = View.GONE } } holder.binding.childRV.apply { recycledViewPool.setMaxRecycledViews(0, 0) adapter = homeChildRVAdapter Loading Loading
app/build.gradle +1 −2 Original line number Diff line number Diff line Loading @@ -135,8 +135,7 @@ dependencies { // Optional -- mockito-kotlin testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0" testImplementation 'org.mockito:mockito-inline:2.13.0' testImplementation "androidx.arch.core:core-testing:2.1.0" // Coil and PhotoView implementation "io.coil-kt:coil:1.4.0" Loading
app/src/main/java/foundation/e/apps/api/DownloadManager.kt +0 −1 Original line number Diff line number Diff line Loading @@ -125,7 +125,6 @@ class DownloadManager @Inject constructor( } catch (e: Exception) { Timber.e(e) } } private fun tickerFlow(downloadId: Long, period: Duration, initialDelay: Duration = Duration.ZERO) = flow { Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +296 −195 Original line number Diff line number Diff line Loading @@ -144,23 +144,28 @@ class FusedAPIImpl @Inject constructor( * To prevent waiting so long and fail early, use withTimeout{}. */ withTimeout(timeoutDurationInMillis) { if (applicationType != APP_TYPE_ANY) { val response = if (applicationType == APP_TYPE_OPEN) { cleanAPKRepository.getHomeScreenData( if (preferenceManagerModule.isGplaySelected()) { list.addAll(fetchGPlayHome(authData)) } if (preferenceManagerModule.isOpenSourceSelected()) { val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_ANY, CleanAPKInterface.APP_SOURCE_FOSS ).body() } else { cleanAPKRepository.getHomeScreenData( response?.home?.let { list.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN)) } } if (preferenceManagerModule.isPWASelected()) { val response = cleanAPKRepository.getHomeScreenData( CleanAPKInterface.APP_TYPE_PWA, CleanAPKInterface.APP_SOURCE_ANY ).body() } response?.home?.let { list.addAll(generateCleanAPKHome(it, applicationType)) list.addAll(generateCleanAPKHome(it, APP_TYPE_PWA)) } } else { list.addAll(fetchGPlayHome(authData)) } } } catch (e: TimeoutCancellationException) { Loading Loading @@ -194,20 +199,12 @@ class FusedAPIImpl @Inject constructor( var apiStatus: ResultStatus = ResultStatus.OK var applicationCategoryType = preferredApplicationType if (preferredApplicationType != APP_TYPE_ANY) { handleCleanApkCategories(preferredApplicationType, categoriesList, type).run { if (this != ResultStatus.OK) { apiStatus = this } } } else { handleAllSourcesCategories(categoriesList, type, authData).run { if (first != ResultStatus.OK) { apiStatus = first applicationCategoryType = second } } } categoriesList.sortBy { item -> item.title.lowercase() } return Triple(categoriesList, applicationCategoryType, apiStatus) } Loading @@ -231,33 +228,149 @@ class FusedAPIImpl @Inject constructor( */ return liveData { val packageSpecificResults = ArrayList<FusedApp>() var gplayPackageResult: FusedApp? = null var cleanapkPackageResult: FusedApp? = null fetchPackageSpecificResult(authData, query, packageSpecificResults)?.let { if (it.data?.second == true) { // if there are no data to load emit(it) return@liveData } } val status = runCodeBlockWithTimeout({ if (preferenceManagerModule.preferredApplicationType() == APP_TYPE_ANY) { try { /* * Surrounding with try-catch because if query is not a package name, * then GPlay throws an error. */ getApplicationDetails(query, query, authData, Origin.GPLAY).let { if (it.second == ResultStatus.OK) { gplayPackageResult = it.first val searchResult = mutableListOf<FusedApp>() val cleanApkResults = mutableListOf<FusedApp>() if (preferenceManagerModule.isOpenSourceSelected()) { fetchOpenSourceSearchResult( this@FusedAPIImpl, cleanApkResults, query, searchResult, packageSpecificResults )?.let { emit(it) } } if (preferenceManagerModule.isGplaySelected()) { emitSource( fetchGplaySearchResults( query, authData, searchResult, packageSpecificResults ) ) } } catch (e: Exception) { Timber.e(e) if (preferenceManagerModule.isPWASelected()) { fetchPWASearchResult( this@FusedAPIImpl, query, searchResult, packageSpecificResults )?.let { emit(it) } } } getCleanapkSearchResult(query).let { /* Cleanapk always returns something, it is never null. * If nothing is found, it returns a blank FusedApp() object. * Blank result to be filtered out. */ if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { cleanapkPackageResult = it.data!! } private suspend fun fetchPWASearchResult( fusedAPIImpl: FusedAPIImpl, query: String, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): 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 { if (this.isNotEmpty()) { pwaApps.addAll(this) } } }) if (pwaApps.isNotEmpty() || status != ResultStatus.OK) { searchResult.addAll(pwaApps) return ResultSupreme.create( status, Pair( filterWithKeywordSearch( searchResult, packageSpecificResults, query ), false ) ) } return null } private fun fetchGplaySearchResults( query: String, authData: AuthData, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> = getGplaySearchResults(query, authData).map { if (it.first.isNotEmpty()) { searchResult.addAll(it.first) } ResultSupreme.Success( Pair( filterWithKeywordSearch( searchResult, packageSpecificResults, query ), it.second ) ) } private suspend fun fetchOpenSourceSearchResult( fusedAPIImpl: FusedAPIImpl, cleanApkResults: MutableList<FusedApp>, query: String, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? { val status = fusedAPIImpl.runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) { searchResult.addAll(cleanApkResults) return ResultSupreme.create( status, Pair( filterWithKeywordSearch( searchResult, packageSpecificResults, query ), preferenceManagerModule.isGplaySelected() || preferenceManagerModule.isPWASelected() ) ) } return null } private suspend fun fetchPackageSpecificResult( authData: AuthData, query: String, packageSpecificResults: MutableList<FusedApp> ): ResultSupreme<Pair<List<FusedApp>, Boolean>>? { var gplayPackageResult: FusedApp? = null var cleanapkPackageResult: FusedApp? = null val status = runCodeBlockWithTimeout({ if (preferenceManagerModule.isGplaySelected()) { gplayPackageResult = getGplayPackagResult(query, authData) } if (preferenceManagerModule.isOpenSourceSelected()) { cleanapkPackageResult = getCleanApkPackageResult(query) } }) Loading @@ -275,8 +388,9 @@ class FusedAPIImpl @Inject constructor( * Also send true in the pair to signal more results being loaded. */ if (status != ResultStatus.OK) { emit(ResultSupreme.create(status, Pair(packageSpecificResults, true))) return@liveData return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } return ResultSupreme.create(status, Pair(packageSpecificResults, false)) } /* Loading @@ -287,81 +401,41 @@ class FusedAPIImpl @Inject constructor( * But for the other keyword related search results, we do not allow duplicate package names. * We also filter out apps which are already present in packageSpecificResults list. */ fun filterWithKeywordSearch(list: List<FusedApp>): List<FusedApp> { private fun filterWithKeywordSearch( list: List<FusedApp>, packageSpecificResults: List<FusedApp>, query: String ): List<FusedApp> { val filteredResults = list.distinctBy { it.package_name } .filter { packageSpecificResults.isEmpty() || it.package_name != query } return packageSpecificResults + filteredResults } val cleanApkResults = mutableListOf<FusedApp>() when (preferenceManagerModule.preferredApplicationType()) { APP_TYPE_ANY -> { val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) if (cleanApkResults.isNotEmpty() || status != ResultStatus.OK) { /* * If cleanapk results are empty, dont emit emit data as it may * briefly show "No apps found..." * If status is timeout, then do emit the value. * Send true in the pair to signal more results (i.e from GPlay) being loaded. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), true) ) ) } emitSource( getGplayAndCleanapkCombinedResults(query, authData, cleanApkResults).map { /* * We are assuming that there will be no timeout here. * If there had to be any timeout, it would already have happened * while fetching package specific results. */ ResultSupreme.Success( Pair( filterWithKeywordSearch(it.first), it.second ) ) private suspend fun getCleanApkPackageResult( query: String, ): FusedApp? { getCleanapkSearchResult(query).let { if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { return it.data!! } ) } APP_TYPE_OPEN -> { val status = runCodeBlockWithTimeout({ cleanApkResults.addAll(getCleanAPKSearchResults(query)) }) /* * Send false in pair to signal no more results to load, as only cleanapk * results are fetched, we don't have to wait for GPlay results. */ emit( ResultSupreme.create( status, Pair(filterWithKeywordSearch(cleanApkResults), false) ) ) return null } APP_TYPE_PWA -> { val status = runCodeBlockWithTimeout({ cleanApkResults.addAll( getCleanAPKSearchResults( query, CleanAPKInterface.APP_SOURCE_ANY, CleanAPKInterface.APP_TYPE_PWA ) ) }) /* * Send false in pair to signal no more results to load, as only cleanapk * results are fetched for PWAs. */ emit(ResultSupreme.create(status, Pair(cleanApkResults, false))) private suspend fun getGplayPackagResult( query: String, authData: AuthData, ): FusedApp? { try { getApplicationDetails(query, query, authData, Origin.GPLAY).let { if (it.second == ResultStatus.OK) { return it.first } } } catch (e: Exception) { Timber.e(e) } return null } /* Loading Loading @@ -409,7 +483,13 @@ class FusedAPIImpl @Inject constructor( versionCode: Int, offerType: Int ): String? { val list = gPlayAPIRepository.getOnDemandModule(packageName, moduleName, versionCode, offerType, authData) val list = gPlayAPIRepository.getOnDemandModule( packageName, moduleName, versionCode, offerType, authData ) for (element in list) { if (element.name == "$moduleName.apk") { return element.url Loading @@ -418,7 +498,6 @@ class FusedAPIImpl @Inject constructor( return null } suspend fun updateFusedDownloadWithDownloadingInfo( authData: AuthData, origin: Origin, Loading Loading @@ -832,39 +911,69 @@ class FusedAPIImpl @Inject constructor( type: Category.Type, authData: AuthData ): Pair<ResultStatus, String> { var data: Categories? = null var apiStatus = ResultStatus.OK var errorApplicationCategory = "" /* * Try within timeout limit for open source native apps categories. */ runCodeBlockWithTimeout({ data = getOpenSourceCategories() data?.let { categoriesList.addAll( getFusedCategoryBasedOnCategoryType( it, if (preferenceManagerModule.isOpenSourceSelected()) { val openSourceCategoryResult = fetchOpenSourceCategories(type) categoriesList.addAll(openSourceCategoryResult.second) apiStatus = openSourceCategoryResult.first errorApplicationCategory = openSourceCategoryResult.third } if (preferenceManagerModule.isPWASelected()) { val pwaCategoriesResult = fetchPWACategories(type) categoriesList.addAll(pwaCategoriesResult.second) apiStatus = pwaCategoriesResult.first errorApplicationCategory = pwaCategoriesResult.third } if (preferenceManagerModule.isGplaySelected()) { val gplayCategoryResult = fetchGplayCategories( type, AppTag.OpenSource(context.getString(R.string.open_source)) ) authData ) categoriesList.addAll(gplayCategoryResult.second) apiStatus = gplayCategoryResult.first errorApplicationCategory = gplayCategoryResult.third } return Pair(apiStatus, errorApplicationCategory) } private suspend fun FusedAPIImpl.fetchGplayCategories( type: Category.Type, authData: AuthData, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus = ResultStatus.OK val categoryList = mutableListOf<FusedCategory>() runCodeBlockWithTimeout({ val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category, app) category } categoryList.addAll(playResponse) }, { errorApplicationCategory = APP_TYPE_OPEN errorApplicationCategory = APP_TYPE_ANY apiStatus = ResultStatus.TIMEOUT }, { errorApplicationCategory = APP_TYPE_OPEN errorApplicationCategory = APP_TYPE_ANY apiStatus = ResultStatus.UNKNOWN }) return Triple(apiStatus, categoryList, errorApplicationCategory) } /* * Try within timeout limit to get PWA categories */ private suspend fun FusedAPIImpl.fetchPWACategories( type: Category.Type, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoriesList = mutableListOf<FusedCategory>() runCodeBlockWithTimeout({ data = getPWAsCategories() data?.let { categoriesList.addAll( getPWAsCategories()?.let { fusedCategoriesList.addAll( getFusedCategoryBasedOnCategoryType( it, type, AppTag.PWA(context.getString(R.string.pwa)) ) Loading @@ -877,26 +986,33 @@ class FusedAPIImpl @Inject constructor( errorApplicationCategory = APP_TYPE_PWA apiStatus = ResultStatus.UNKNOWN }) return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) } /* * Try within timeout limit to get native app categories from Play Store */ private suspend fun FusedAPIImpl.fetchOpenSourceCategories( type: Category.Type, ): Triple<ResultStatus, List<FusedCategory>, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK val fusedCategoryList = mutableListOf<FusedCategory>() runCodeBlockWithTimeout({ val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category, app) category getOpenSourceCategories()?.let { fusedCategoryList.addAll( getFusedCategoryBasedOnCategoryType( it, type, AppTag.OpenSource(context.getString(R.string.open_source)) ) ) } categoriesList.addAll(playResponse) }, { errorApplicationCategory = APP_TYPE_ANY errorApplicationCategory = APP_TYPE_OPEN apiStatus = ResultStatus.TIMEOUT }, { errorApplicationCategory = APP_TYPE_ANY errorApplicationCategory = APP_TYPE_OPEN apiStatus = ResultStatus.UNKNOWN }) return Pair(apiStatus, errorApplicationCategory) return Triple(apiStatus, fusedCategoryList, errorApplicationCategory) } /** Loading Loading @@ -1077,24 +1193,6 @@ class FusedAPIImpl @Inject constructor( return list } /* * Function to return a livedata with value from cleanapk and Google Play store combined. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ private fun getGplayAndCleanapkCombinedResults( query: String, authData: AuthData, cleanApkResults: List<FusedApp> ): LiveData<Pair<List<FusedApp>, Boolean>> { val localList = ArrayList<FusedApp>(cleanApkResults) return getGplaySearchResults(query, authData).map { pair -> Pair( localList.apply { addAll(pair.first) }.distinctBy { it.package_name }, pair.second ) } } private fun getGplaySearchResults( query: String, authData: AuthData Loading @@ -1112,9 +1210,9 @@ class FusedAPIImpl @Inject constructor( * Home screen-related internal functions */ private suspend fun generateCleanAPKHome(home: Home, prefType: String): List<FusedHome> { private suspend fun generateCleanAPKHome(home: Home, appType: String): List<FusedHome> { val list = mutableListOf<FusedHome>() val headings = if (prefType == APP_TYPE_OPEN) { val headings = if (appType == APP_TYPE_OPEN) { mapOf( "top_updated_apps" to context.getString(R.string.top_updated_apps), "top_updated_games" to context.getString(R.string.top_updated_games), Loading Loading @@ -1203,7 +1301,10 @@ class FusedAPIImpl @Inject constructor( } } } return list return list.map { it.source = appType it } } private suspend fun fetchGPlayHome(authData: AuthData): List<FusedHome> { Loading
app/src/main/java/foundation/e/apps/api/fused/data/FusedHome.kt +2 −1 Original line number Diff line number Diff line Loading @@ -20,5 +20,6 @@ package foundation.e.apps.api.fused.data data class FusedHome( val title: String = String(), val list: List<FusedApp> = emptyList() val list: List<FusedApp> = emptyList(), var source: String = String() )
app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt +17 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package foundation.e.apps.home.model import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager Loading @@ -26,6 +27,8 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIImpl import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedHome Loading Loading @@ -70,6 +73,20 @@ class HomeParentRVAdapter( homeChildRVAdapter.setData(fusedHome.list) holder.binding.titleTV.text = fusedHome.title when (fusedHome.source) { FusedAPIImpl.APP_TYPE_OPEN -> { holder.binding.categoryTag.visibility = View.VISIBLE holder.binding.categoryTag.text = holder.binding.root.context.getString(R.string.open_source) } FusedAPIImpl.APP_TYPE_PWA -> { holder.binding.categoryTag.visibility = View.VISIBLE holder.binding.categoryTag.text = holder.binding.root.context.getString(R.string.pwa) } else -> { holder.binding.categoryTag.visibility = View.GONE } } holder.binding.childRV.apply { recycledViewPool.setMaxRecycledViews(0, 0) adapter = homeChildRVAdapter Loading