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

Commit 2ca13f69 authored by Hasib Prince's avatar Hasib Prince
Browse files

Merge branch '1549-loading_search_page' into 'main'

1549 loading search page

See merge request !354
parents 50b76170 ae359801
Loading
Loading
Loading
Loading
Loading
+11 −3
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ package foundation.e.apps.data.fused
import androidx.lifecycle.LiveData
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.SearchBundle
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.Origin
@@ -107,11 +108,18 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedApi)
        return fusedAPIImpl.getSearchSuggestions(query)
    }

    fun getSearchResults(
    suspend fun getCleanApkSearchResults(
        query: String,
        authData: AuthData
    ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> {
        return fusedAPIImpl.getSearchResults(query, authData)
    ): ResultSupreme<Pair<List<FusedApp>, Boolean>> {
        return fusedAPIImpl.getCleanApkSearchResults(query, authData)
    }

    suspend fun getGplaySearchResults(
        query: String,
        nextPageSubBundle: Set<SearchBundle.SubBundle>?
    ): GplaySearchResult {
        return fusedAPIImpl.getGplaySearchResult(query, nextPageSubBundle)
    }

    suspend fun getAppsListBasedOnCategory(
+12 −5
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
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 foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.cleanapk.data.download.Download
import foundation.e.apps.data.enums.FilterLevel
@@ -17,6 +18,8 @@ import foundation.e.apps.data.fused.utils.CategoryType
import foundation.e.apps.data.fusedDownload.models.FusedDownload
import retrofit2.Response

typealias GplaySearchResult = ResultSupreme<Pair<List<FusedApp>, Set<SearchBundle.SubBundle>>>

interface FusedApi {
    companion object {
        const val APP_TYPE_ANY = "any"
@@ -55,14 +58,18 @@ interface FusedApi {
     * Fetches search results from cleanAPK and GPlay servers and returns them
     * @param query Query
     * @param authData [AuthData]
     * @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.
     * @return ResultSupreme which contains a Pair<List<FusedApp>, Boolean> where List<FusedApp>
     *     is the app list and [Boolean] indicates more data to load or not.
     */
    fun getSearchResults(
    suspend fun getCleanApkSearchResults(
        query: String,
        authData: AuthData
    ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>>
    ): ResultSupreme<Pair<List<FusedApp>, Boolean>>

    suspend fun getGplaySearchResult(
        query: String,
        nextPageSubBundle: Set<SearchBundle.SubBundle>?
    ): GplaySearchResult

    suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry>

+69 −87
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Artwork
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.SearchBundle
import com.aurora.gplayapi.data.models.StreamCluster
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
@@ -62,7 +63,8 @@ import foundation.e.apps.data.fused.utils.CategoryType
import foundation.e.apps.data.fused.utils.CategoryUtils
import foundation.e.apps.data.fusedDownload.models.FusedDownload
import foundation.e.apps.data.gplay.GplayStoreRepository
import foundation.e.apps.data.gplay.utils.runFlowWithTimeout
import foundation.e.apps.data.gplay.utils.GplayHttpRequestException
import foundation.e.apps.data.login.exceptions.GPlayException
import foundation.e.apps.data.preference.PreferenceManagerModule
import foundation.e.apps.install.pkg.PWAManagerModule
import foundation.e.apps.install.pkg.PkgManagerModule
@@ -71,16 +73,15 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withTimeout
import retrofit2.Response
import timber.log.Timber
import java.net.SocketTimeoutException
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

typealias GplaySearchResultLiveData = LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>>
typealias FusedHomeDeferred = Deferred<ResultSupreme<List<FusedHome>>>

@Singleton
@@ -99,6 +100,8 @@ class FusedApiImpl @Inject constructor(
        private const val CATEGORY_TITLE_REPLACEABLE_CONJUNCTION = "&"
        private const val CATEGORY_OPEN_GAMES_ID = "game_open_games"
        private const val CATEGORY_OPEN_GAMES_TITLE = "Open games"
        private const val ERROR_GPLAY_SEARCH = "Gplay search has failed!"
        private const val ERROR_GPLAY_SOURCE_NOT_SELECTED = "Gplay apps are not selected!"
    }

    /**
@@ -244,22 +247,21 @@ class FusedApiImpl @Inject constructor(
     * a Boolean signifying if more search results are being loaded.
     * Observe this livedata to display new apps as they are fetched from the network.
     */
    override fun getSearchResults(
    override suspend fun getCleanApkSearchResults(
        query: String,
        authData: AuthData
    ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> {
    ): 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>()
        var finalSearchResult: ResultSupreme<Pair<List<FusedApp>, Boolean>> = ResultSupreme.Error()

        fetchPackageSpecificResult(authData, query, packageSpecificResults).let {
            if (it.data?.second != true) { // if there are no data to load
                    emit(it)
                    return@liveData
                return it
            }
        }

@@ -267,44 +269,32 @@ class FusedApiImpl @Inject constructor(
        val cleanApkResults = mutableListOf<FusedApp>()

        if (preferenceManagerModule.isOpenSourceSelected()) {
                fetchOpenSourceSearchResult(
                    this@FusedApiImpl,
            finalSearchResult = fetchOpenSourceSearchResult(
                cleanApkResults,
                query,
                searchResult,
                packageSpecificResults
                ).let { emit(it) }
            )
        }

        if (preferenceManagerModule.isPWASelected()) {
                fetchPWASearchResult(
                    this@FusedApiImpl,
                    query,
                    searchResult,
                    packageSpecificResults
                ).let { emit(it) }
            }

            if (preferenceManagerModule.isGplaySelected()) {
                emitSource(
                    fetchGplaySearchResults(
            finalSearchResult = fetchPWASearchResult(
                query,
                searchResult,
                packageSpecificResults
            )
                )
            }
        }

        return finalSearchResult
    }

    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.runCodeWithTimeout({
        val status = runCodeWithTimeout({
            val apps =
                cleanApkPWARepository.getSearchResult(query).body()?.apps
            apps?.apply {
@@ -331,46 +321,13 @@ class FusedApiImpl @Inject constructor(
        )
    }

    private suspend fun fetchGplaySearchResults(
        query: String,
        searchResult: MutableList<FusedApp>,
        packageSpecificResults: ArrayList<FusedApp>
    ): GplaySearchResultLiveData {
        return runFlowWithTimeout(
            {
                getGplaySearchResult(query)
            }, {
            it.second
        }, {
            Pair(listOf(), false) // empty data for timeout
        }
        ).map {
            if (it.isSuccess()) {
                searchResult.addAll(it.data!!.first)
                ResultSupreme.Success(
                    Pair(
                        filterWithKeywordSearch(
                            searchResult,
                            packageSpecificResults,
                            query
                        ),
                        it.data!!.second
                    )
                )
            } else {
                it
            }
        }
    }

    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.runCodeWithTimeout({
        val status = runCodeWithTimeout({
            cleanApkResults.addAll(getCleanAPKSearchResults(query))
        })

@@ -1115,16 +1072,39 @@ class FusedApiImpl @Inject constructor(
        return list
    }

    private suspend fun getGplaySearchResult(
    override suspend fun getGplaySearchResult(
        query: String,
    ): Flow<Pair<List<FusedApp>, Boolean>> {
        val searchResults = gplayRepository.getSearchResult(query)
        return searchResults.map {
            val fusedAppList = it.first.map { app -> replaceWithFDroid(app) }
            Pair(
                fusedAppList,
                it.second
            )
        nextPageSubBundle: Set<SearchBundle.SubBundle>?
    ): GplaySearchResult {
        try {
            val searchResults =
                gplayRepository.getSearchResult(query, nextPageSubBundle?.toMutableSet())

            if (!preferenceManagerModule.isGplaySelected()) {
                return ResultSupreme.Error(ERROR_GPLAY_SOURCE_NOT_SELECTED)
            }

            val fusedAppList =
                searchResults.first.map { app -> replaceWithFDroid(app) }.toMutableList()

            if (searchResults.second.isNotEmpty()) {
                fusedAppList.add(FusedApp(isPlaceHolder = true))
            }

            return ResultSupreme.Success(Pair(fusedAppList.toList(), searchResults.second.toSet()))
        } catch (e: GplayHttpRequestException) {
            val message = (
                e.localizedMessage?.ifBlank { ERROR_GPLAY_SEARCH }
                    ?: ERROR_GPLAY_SEARCH
                ) + "Status: ${e.status}"

            val exception = GPlayException(e.status == 408, message)
            return ResultSupreme.Error(message, exception)
        } catch (e: Exception) {
            val exception =
                GPlayException(e is SocketTimeoutException, e.localizedMessage)

            return ResultSupreme.Error(e.localizedMessage ?: "", exception)
        }
    }

@@ -1431,7 +1411,9 @@ class FusedApiImpl @Inject constructor(
        var nextPageUrl = ""

        val status = runCodeWithTimeout({
            val streamCluster = gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster
            val streamCluster =
                gplayRepository.getAppsByCategory(category, pageUrl) as StreamCluster

            val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList)
            filteredAppList.data?.let {
                fusedAppList = it.toMutableList()
+2 −2
Original line number Diff line number Diff line
@@ -22,12 +22,12 @@ import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.File
import com.aurora.gplayapi.data.models.SearchBundle
import foundation.e.apps.data.BaseStoreRepository
import foundation.e.apps.data.fused.utils.CategoryType
import kotlinx.coroutines.flow.Flow

interface GplayStoreRepository : BaseStoreRepository {
    suspend fun getSearchResult(query: String): Flow<Pair<List<App>, Boolean>>
    suspend fun getSearchResult(query: String, subBundle: MutableSet<SearchBundle.SubBundle>?): Pair<List<App>, MutableSet<SearchBundle.SubBundle>>
    suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry>
    suspend fun getAppsByCategory(category: String, pageUrl: String? = null): Any
    suspend fun getCategories(type: CategoryType? = null): List<Category>
+22 −116
Original line number Diff line number Diff line
@@ -39,11 +39,8 @@ import foundation.e.apps.data.fused.utils.CategoryType
import foundation.e.apps.data.gplay.utils.GPlayHttpClient
import foundation.e.apps.data.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 timber.log.Timber
import javax.inject.Inject

class GplayStoreRepositoryImpl @Inject constructor(
@@ -78,75 +75,30 @@ class GplayStoreRepositoryImpl @Inject constructor(

    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
             */
            var authData = loginSourceRepository.gplayAuth ?: return@flow

        subBundle: MutableSet<SearchBundle.SubBundle>?
    ): Pair<List<App>, MutableSet<SearchBundle.SubBundle>> {
        var authData = loginSourceRepository.gplayAuth ?: return Pair(emptyList(), mutableSetOf())
        val searchHelper =
            SearchHelper(authData).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())
        Timber.d("Fetching search result for $query, subBundle: $subBundle")

            /*
             * 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)
        subBundle?.let {
            val searchResult = searchHelper.next(it)
            Timber.d("fetching next page search data...")
            return getSearchResultPair(searchResult)
        }

    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(),
                )
        val searchResult = searchHelper.searchResults(query)
        return getSearchResultPair(searchResult)
    }
        }
        return nextSubBundleSet

    private fun getSearchResultPair(
        searchBundle: SearchBundle
    ): Pair<MutableList<App>, MutableSet<SearchBundle.SubBundle>> {
        val apps = searchBundle.appList
        Timber.d("Search result is found: ${apps.size}")
        return Pair(apps, searchBundle.subBundles)
    }

    override suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry> {
@@ -214,52 +166,6 @@ class GplayStoreRepositoryImpl @Inject constructor(
        return if (type == CategoryType.APPLICATION) Category.Type.APPLICATION else Category.Type.GAME
    }

    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: Chart,
Loading