Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4bc26e6d authored by Hasib Prince's avatar Hasib Prince
Browse files

App Lounge: Resolved conlflict: Merging main -> 5587-crash_app_startup

parents 83b30add 9bf99950
Loading
Loading
Loading
Loading
Loading
+191 −17
Original line number Diff line number Diff line
@@ -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
@@ -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> {
@@ -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,
@@ -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
            )

        }
    }

+6 −4
Original line number Diff line number Diff line
@@ -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
@@ -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,
@@ -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)
    }

+32 −18
Original line number Diff line number Diff line
@@ -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
@@ -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(
+2 −1
Original line number Diff line number Diff line
@@ -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
@@ -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)
    }

+45 −9
Original line number Diff line number Diff line
@@ -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

@@ -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)
@@ -156,6 +183,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) {

            dismissTimeoutDialog()

            isDetailsLoaded = true
            if (applicationViewModel.appStatus.value == null) {
                applicationViewModel.appStatus.value = it.status
            }
@@ -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)
@@ -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(
@@ -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