Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +191 −17 Original line number Diff line number Diff line Loading @@ -21,6 +21,9 @@ package foundation.e.apps.api.fused import android.content.Context import android.text.format.Formatter import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App Loading Loading @@ -205,32 +208,156 @@ class FusedAPIImpl @Inject constructor( * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query * @param authData [AuthData] * @return A list of nullable [FusedApp] * @return A livedata Pair of list of non-nullable [FusedApp] and * a Boolean signifying if more search results are being loaded. * Observe this livedata to display new apps as they are fetched from the network. */ suspend fun getSearchResults(query: String, authData: AuthData): Pair<List<FusedApp>, ResultStatus> { val fusedResponse = mutableListOf<FusedApp>() fun getSearchResults( query: String, authData: AuthData ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> { /* * Returning livedata to improve performance, so that we do not have to wait forever * for all results to be fetched from network before showing them. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ return liveData { val packageSpecificResults = ArrayList<FusedApp>() 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) { packageSpecificResults.add(it.first) } } } catch (_: Exception) {} } 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()) { packageSpecificResults.add(it.data!!) } } }) /* * If there was a timeout, return it and don't try to fetch anything else. * 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 } /* * The list packageSpecificResults may contain apps with duplicate package names. * Example, "org.telegram.messenger" will result in "Telegram" app from Play Store * and "Telegram FOSS" from F-droid. We show both of them at the top. * * 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> { 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 -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) fusedResponse.addAll(getGplaySearchResults(query, authData)) 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)) } ) } APP_TYPE_OPEN -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) 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) ) ) } APP_TYPE_PWA -> { fusedResponse.addAll( 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))) } } } } /* * Method to search cleanapk based on package name. * This is to be only used for showing an entry in search results list. * DO NOT use this to show info on ApplicationFragment as it will not have all the required * information to show for an app. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/2629 */ private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme<FusedApp> { var fusedApp = FusedApp() val status = runCodeBlockWithTimeout({ val result = cleanAPKRepository.searchApps( keyword = packageName, by = "package_name" ).body() if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = result.apps[0] } }) return Pair(fusedResponse.distinctBy { it.package_name }, status) return ResultSupreme.create(status, fusedApp) } suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> { Loading Loading @@ -349,6 +476,28 @@ class FusedAPIImpl @Inject constructor( return ResultSupreme.create(status, list) } /* * Function to search cleanapk using package name. * Will be used to handle f-droid deeplink. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ suspend fun getCleanapkAppDetails(packageName: String): Pair<FusedApp, ResultStatus> { var fusedApp = FusedApp() val status = runCodeBlockWithTimeout({ val result = cleanAPKRepository.searchApps( keyword = packageName, by = "package_name" ).body() if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = cleanAPKRepository.getAppOrPWADetailsByID(result.apps[0]._id).body()?.app ?: FusedApp() } }) return Pair(fusedApp, status) } suspend fun getApplicationDetails( packageNameList: List<String>, authData: AuthData, Loading Loading @@ -806,10 +955,35 @@ class FusedAPIImpl @Inject constructor( return list } private suspend fun getGplaySearchResults(query: String, authData: AuthData): List<FusedApp> { /* * 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 ): LiveData<Pair<List<FusedApp>, Boolean>> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData) return searchResults.map { app -> app.transformToFusedApp() return searchResults.map { Pair( it.first.map { app -> app.transformToFusedApp() }, it.second ) } } Loading app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +6 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package foundation.e.apps.api.fused import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -81,6 +82,10 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getApplicationDetails(id, packageName, authData, origin) } suspend fun getCleanapkAppDetails(packageName: String): Pair<FusedApp, ResultStatus> { return fusedAPIImpl.getCleanapkAppDetails(packageName) } suspend fun updateFusedDownloadWithDownloadingInfo( authData: AuthData, origin: Origin, Loading Loading @@ -112,10 +117,7 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.fetchAuthData(email, aasToken) } suspend fun getSearchResults( query: String, authData: AuthData ): Pair<List<FusedApp>, ResultStatus> { fun getSearchResults(query: String, authData: AuthData): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> { return fusedAPIImpl.getSearchResults(query, authData) } Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +32 −18 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ package foundation.e.apps.api.gplay import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -104,29 +106,41 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { val searchData = mutableListOf<App>() /** * Sends livedata of list of apps being loaded from search and a boolean * signifying if more data is to be loaded. */ fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { /* * Send livedata to improve UI performance, so we don't have to wait for loading all results. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ return liveData { withContext(Dispatchers.IO) { /* * Variable names and logic made same as that of Aurora store. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ val searchHelper = SearchHelper(authData).using(gPlayHttpClient) val searchResult = searchHelper.searchResults(query) searchData.addAll(searchResult.appList) val searchBundle = searchHelper.searchResults(query) emit(Pair(searchBundle.appList, true)) // Fetch more results in case the given result is a promoted app if (searchData.size == 1) { val bundleSet: MutableSet<SearchBundle.SubBundle> = searchResult.subBundles var nextSubBundleSet: MutableSet<SearchBundle.SubBundle> do { val searchBundle = searchHelper.next(bundleSet) if (searchBundle.appList.isNotEmpty()) { searchData.addAll(searchBundle.appList) nextSubBundleSet = searchBundle.subBundles val newSearchBundle = searchHelper.next(nextSubBundleSet) if (newSearchBundle.appList.isNotEmpty()) { searchBundle.apply { subBundles.clear() subBundles.addAll(newSearchBundle.subBundles) appList.addAll(newSearchBundle.appList) emit(Pair(searchBundle.appList, nextSubBundleSet.isNotEmpty())) } bundleSet.apply { clear() addAll(searchBundle.subBundles) } } while (bundleSet.isNotEmpty()) } while (nextSubBundleSet.isNotEmpty()) } } return searchData } suspend fun getDownloadInfo( Loading app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +2 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package foundation.e.apps.api.gplay import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -48,7 +49,7 @@ class GPlayAPIRepository @Inject constructor( return gPlayAPIImpl.getSearchSuggestions(query, authData) } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { return gPlayAPIImpl.getSearchResults(query, authData) } Loading app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +45 −9 Original line number Diff line number Diff line Loading @@ -76,6 +76,33 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private var _binding: FragmentApplicationBinding? = null private val binding get() = _binding!! /* * We have no way to pass an argument for a specific deeplink to signify it is an f-droid link. * Hence we check the intent from the activity. * This boolean is later used to lock the origin to Origin.CLEANAPK, * and call a different method to fetch from cleanapk. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ private val isFdroidDeepLink: Boolean by lazy { activity?.intent?.data?.host?.equals("f-droid.org") ?: false } /* * We will use this variable in all cases instead of directly calling args.origin. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ private val origin by lazy { if (isFdroidDeepLink) { Origin.CLEANAPK } else { args.origin } } private var isDetailsLoaded = false @Inject lateinit var pkgManagerModule: PkgManagerModule Loading Loading @@ -125,7 +152,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { val notAvailable = getString(R.string.not_available) val screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(args.origin) val screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) binding.recyclerView.apply { adapter = screenshotsRVAdapter layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) Loading Loading @@ -156,6 +183,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { dismissTimeoutDialog() isDetailsLoaded = true if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status } Loading @@ -168,7 +196,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appAuthor.text = it.author appInfoFetchViewModel.setAuthorNameIfNeeded(appAuthor, it) categoryTitle.text = it.category if (args.origin == Origin.CLEANAPK) { if (origin == Origin.CLEANAPK) { appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) } else { appIcon.load(it.icon_image_path) Loading Loading @@ -227,7 +255,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { binding.infoInclude.apply { appUpdatedOn.text = getString( R.string.updated_on, if (args.origin == Origin.CLEANAPK) it.updatedOn else it.last_modified if (origin == Origin.CLEANAPK) it.updatedOn else it.last_modified ) appRequires.text = getString(R.string.min_android_version, notAvailable) appVersion.text = getString( Loading Loading @@ -308,14 +336,22 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } override fun refreshData(authData: AuthData) { if (isDetailsLoaded) return /* Show the loading bar. */ showLoadingUI() /* Remove trailing slash (if present) that can become part of the packageName */ val packageName = args.packageName.run { if (endsWith('/')) dropLast(1) else this } if (isFdroidDeepLink) { applicationViewModel.getCleanapkAppDetails(packageName) } else { applicationViewModel.getApplicationDetails( args.id, args.packageName, packageName, authData, args.origin origin ) } } private fun observeDownloadStatus(view: View) { applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> Loading Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +191 −17 Original line number Diff line number Diff line Loading @@ -21,6 +21,9 @@ package foundation.e.apps.api.fused import android.content.Context import android.text.format.Formatter import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App Loading Loading @@ -205,32 +208,156 @@ class FusedAPIImpl @Inject constructor( * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query * @param authData [AuthData] * @return A list of nullable [FusedApp] * @return A livedata Pair of list of non-nullable [FusedApp] and * a Boolean signifying if more search results are being loaded. * Observe this livedata to display new apps as they are fetched from the network. */ suspend fun getSearchResults(query: String, authData: AuthData): Pair<List<FusedApp>, ResultStatus> { val fusedResponse = mutableListOf<FusedApp>() fun getSearchResults( query: String, authData: AuthData ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> { /* * Returning livedata to improve performance, so that we do not have to wait forever * for all results to be fetched from network before showing them. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ return liveData { val packageSpecificResults = ArrayList<FusedApp>() 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) { packageSpecificResults.add(it.first) } } } catch (_: Exception) {} } 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()) { packageSpecificResults.add(it.data!!) } } }) /* * If there was a timeout, return it and don't try to fetch anything else. * 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 } /* * The list packageSpecificResults may contain apps with duplicate package names. * Example, "org.telegram.messenger" will result in "Telegram" app from Play Store * and "Telegram FOSS" from F-droid. We show both of them at the top. * * 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> { 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 -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) fusedResponse.addAll(getGplaySearchResults(query, authData)) 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)) } ) } APP_TYPE_OPEN -> { fusedResponse.addAll(getCleanAPKSearchResults(query)) 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) ) ) } APP_TYPE_PWA -> { fusedResponse.addAll( 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))) } } } } /* * Method to search cleanapk based on package name. * This is to be only used for showing an entry in search results list. * DO NOT use this to show info on ApplicationFragment as it will not have all the required * information to show for an app. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/2629 */ private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme<FusedApp> { var fusedApp = FusedApp() val status = runCodeBlockWithTimeout({ val result = cleanAPKRepository.searchApps( keyword = packageName, by = "package_name" ).body() if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = result.apps[0] } }) return Pair(fusedResponse.distinctBy { it.package_name }, status) return ResultSupreme.create(status, fusedApp) } suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> { Loading Loading @@ -349,6 +476,28 @@ class FusedAPIImpl @Inject constructor( return ResultSupreme.create(status, list) } /* * Function to search cleanapk using package name. * Will be used to handle f-droid deeplink. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ suspend fun getCleanapkAppDetails(packageName: String): Pair<FusedApp, ResultStatus> { var fusedApp = FusedApp() val status = runCodeBlockWithTimeout({ val result = cleanAPKRepository.searchApps( keyword = packageName, by = "package_name" ).body() if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = cleanAPKRepository.getAppOrPWADetailsByID(result.apps[0]._id).body()?.app ?: FusedApp() } }) return Pair(fusedApp, status) } suspend fun getApplicationDetails( packageNameList: List<String>, authData: AuthData, Loading Loading @@ -806,10 +955,35 @@ class FusedAPIImpl @Inject constructor( return list } private suspend fun getGplaySearchResults(query: String, authData: AuthData): List<FusedApp> { /* * 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 ): LiveData<Pair<List<FusedApp>, Boolean>> { val searchResults = gPlayAPIRepository.getSearchResults(query, authData) return searchResults.map { app -> app.transformToFusedApp() return searchResults.map { Pair( it.first.map { app -> app.transformToFusedApp() }, it.second ) } } Loading
app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +6 −4 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package foundation.e.apps.api.fused import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -81,6 +82,10 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getApplicationDetails(id, packageName, authData, origin) } suspend fun getCleanapkAppDetails(packageName: String): Pair<FusedApp, ResultStatus> { return fusedAPIImpl.getCleanapkAppDetails(packageName) } suspend fun updateFusedDownloadWithDownloadingInfo( authData: AuthData, origin: Origin, Loading Loading @@ -112,10 +117,7 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.fetchAuthData(email, aasToken) } suspend fun getSearchResults( query: String, authData: AuthData ): Pair<List<FusedApp>, ResultStatus> { fun getSearchResults(query: String, authData: AuthData): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> { return fusedAPIImpl.getSearchResults(query, authData) } Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIImpl.kt +32 −18 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ package foundation.e.apps.api.gplay import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -104,29 +106,41 @@ class GPlayAPIImpl @Inject constructor( return searchData.filter { it.suggestedQuery.isNotBlank() } } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { val searchData = mutableListOf<App>() /** * Sends livedata of list of apps being loaded from search and a boolean * signifying if more data is to be loaded. */ fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { /* * Send livedata to improve UI performance, so we don't have to wait for loading all results. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ return liveData { withContext(Dispatchers.IO) { /* * Variable names and logic made same as that of Aurora store. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ val searchHelper = SearchHelper(authData).using(gPlayHttpClient) val searchResult = searchHelper.searchResults(query) searchData.addAll(searchResult.appList) val searchBundle = searchHelper.searchResults(query) emit(Pair(searchBundle.appList, true)) // Fetch more results in case the given result is a promoted app if (searchData.size == 1) { val bundleSet: MutableSet<SearchBundle.SubBundle> = searchResult.subBundles var nextSubBundleSet: MutableSet<SearchBundle.SubBundle> do { val searchBundle = searchHelper.next(bundleSet) if (searchBundle.appList.isNotEmpty()) { searchData.addAll(searchBundle.appList) nextSubBundleSet = searchBundle.subBundles val newSearchBundle = searchHelper.next(nextSubBundleSet) if (newSearchBundle.appList.isNotEmpty()) { searchBundle.apply { subBundles.clear() subBundles.addAll(newSearchBundle.subBundles) appList.addAll(newSearchBundle.appList) emit(Pair(searchBundle.appList, nextSubBundleSet.isNotEmpty())) } bundleSet.apply { clear() addAll(searchBundle.subBundles) } } while (bundleSet.isNotEmpty()) } while (nextSubBundleSet.isNotEmpty()) } } return searchData } suspend fun getDownloadInfo( Loading
app/src/main/java/foundation/e/apps/api/gplay/GPlayAPIRepository.kt +2 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ package foundation.e.apps.api.gplay import androidx.lifecycle.LiveData import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData Loading Loading @@ -48,7 +49,7 @@ class GPlayAPIRepository @Inject constructor( return gPlayAPIImpl.getSearchSuggestions(query, authData) } suspend fun getSearchResults(query: String, authData: AuthData): List<App> { fun getSearchResults(query: String, authData: AuthData): LiveData<Pair<List<App>, Boolean>> { return gPlayAPIImpl.getSearchResults(query, authData) } Loading
app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +45 −9 Original line number Diff line number Diff line Loading @@ -76,6 +76,33 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private var _binding: FragmentApplicationBinding? = null private val binding get() = _binding!! /* * We have no way to pass an argument for a specific deeplink to signify it is an f-droid link. * Hence we check the intent from the activity. * This boolean is later used to lock the origin to Origin.CLEANAPK, * and call a different method to fetch from cleanapk. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ private val isFdroidDeepLink: Boolean by lazy { activity?.intent?.data?.host?.equals("f-droid.org") ?: false } /* * We will use this variable in all cases instead of directly calling args.origin. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 */ private val origin by lazy { if (isFdroidDeepLink) { Origin.CLEANAPK } else { args.origin } } private var isDetailsLoaded = false @Inject lateinit var pkgManagerModule: PkgManagerModule Loading Loading @@ -125,7 +152,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { val notAvailable = getString(R.string.not_available) val screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(args.origin) val screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) binding.recyclerView.apply { adapter = screenshotsRVAdapter layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) Loading Loading @@ -156,6 +183,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { dismissTimeoutDialog() isDetailsLoaded = true if (applicationViewModel.appStatus.value == null) { applicationViewModel.appStatus.value = it.status } Loading @@ -168,7 +196,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appAuthor.text = it.author appInfoFetchViewModel.setAuthorNameIfNeeded(appAuthor, it) categoryTitle.text = it.category if (args.origin == Origin.CLEANAPK) { if (origin == Origin.CLEANAPK) { appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) } else { appIcon.load(it.icon_image_path) Loading Loading @@ -227,7 +255,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { binding.infoInclude.apply { appUpdatedOn.text = getString( R.string.updated_on, if (args.origin == Origin.CLEANAPK) it.updatedOn else it.last_modified if (origin == Origin.CLEANAPK) it.updatedOn else it.last_modified ) appRequires.text = getString(R.string.min_android_version, notAvailable) appVersion.text = getString( Loading Loading @@ -308,14 +336,22 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } override fun refreshData(authData: AuthData) { if (isDetailsLoaded) return /* Show the loading bar. */ showLoadingUI() /* Remove trailing slash (if present) that can become part of the packageName */ val packageName = args.packageName.run { if (endsWith('/')) dropLast(1) else this } if (isFdroidDeepLink) { applicationViewModel.getCleanapkAppDetails(packageName) } else { applicationViewModel.getApplicationDetails( args.id, args.packageName, packageName, authData, args.origin origin ) } } private fun observeDownloadStatus(view: View) { applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> Loading