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

Commit 1bdf0ff3 authored by Hasib Prince's avatar Hasib Prince
Browse files

Merge branch '333-refactoring_store_services' into 'epic59-refactoring_store_api'

333 refactoring store services

See merge request !279
parents ac3cadf2 9796ce3c
Loading
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
/*
 * Copyright MURENA SAS 2023
 * Apps  Quickly and easily install Android apps onto your device!
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.apps.api

interface StoreRepository {
    suspend fun getHomeScreenData(): Any
    suspend fun getSearchResult(query: String): Any
    suspend fun getSearchSuggestions(query: String): Any
}
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright MURENA SAS 2023
 * Apps  Quickly and easily install Android apps onto your device!
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.apps.api.cleanapk

import foundation.e.apps.api.StoreRepository
import foundation.e.apps.api.cleanapk.data.home.HomeScreen
import foundation.e.apps.api.cleanapk.data.search.Search
import retrofit2.Response

class CleanApkAppsRepository(
    private val cleanAPKInterface: CleanAPKInterface,
) : StoreRepository {

    override suspend fun getHomeScreenData(): Response<HomeScreen> {
        return cleanAPKInterface.getHomeScreenData(
            CleanAPKInterface.APP_TYPE_ANY,
            CleanAPKInterface.APP_SOURCE_FOSS
        )
    }

    override suspend fun getSearchResult(query: String): Response<Search> {
        return cleanAPKInterface.searchApps(
            query,
            CleanAPKInterface.APP_SOURCE_FOSS,
            CleanAPKInterface.APP_TYPE_ANY,
            20,
            1,
            null
        )
    }

    override suspend fun getSearchSuggestions(query: String): Any {
        TODO("Not yet implemented")
    }
}
+50 −0
Original line number Diff line number Diff line
/*
 * Copyright MURENA SAS 2023
 * Apps  Quickly and easily install Android apps onto your device!
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.apps.api.cleanapk

import foundation.e.apps.api.StoreRepository
import foundation.e.apps.api.cleanapk.data.search.Search
import retrofit2.Response

class CleanApkPWARepository(
    private val cleanAPKInterface: CleanAPKInterface,
) : StoreRepository {

    override suspend fun getHomeScreenData(): Any {
        return cleanAPKInterface.getHomeScreenData(
            CleanAPKInterface.APP_TYPE_PWA,
            CleanAPKInterface.APP_SOURCE_ANY
        )
    }

    override suspend fun getSearchResult(query: String): Response<Search> {
        return cleanAPKInterface.searchApps(
            query,
            CleanAPKInterface.APP_SOURCE_ANY,
            CleanAPKInterface.APP_TYPE_PWA,
            20,
            1,
            null
        )
    }

    override suspend fun getSearchSuggestions(query: String): Any {
        TODO("Not yet implemented")
    }
}
+59 −48
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.Context
import android.text.format.Formatter
import androidx.lifecycle.LiveData
import androidx.lifecycle.LiveDataScope
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.map
import com.aurora.gplayapi.Constants
@@ -32,14 +33,15 @@ import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.Category
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.helpers.TopChartsHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.api.ResultSupreme
import foundation.e.apps.api.StoreRepository
import foundation.e.apps.api.cleanapk.CleanAPKInterface
import foundation.e.apps.api.cleanapk.CleanAPKRepository
import foundation.e.apps.api.cleanapk.data.categories.Categories
import foundation.e.apps.api.cleanapk.data.home.Home
import foundation.e.apps.api.cleanapk.data.home.HomeScreen
import foundation.e.apps.api.cleanapk.data.search.Search
import foundation.e.apps.api.fdroid.FdroidWebInterface
import foundation.e.apps.api.fused.data.FusedApp
@@ -63,11 +65,17 @@ import foundation.e.apps.utils.enums.isUnFiltered
import foundation.e.apps.utils.modules.PWAManagerModule
import foundation.e.apps.utils.modules.PreferenceManagerModule
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withTimeout
import retrofit2.Response
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

typealias GplaySearchResultFlow = Flow<ResultSupreme<Pair<List<FusedApp>, Boolean>>>

@Singleton
class FusedAPIImpl @Inject constructor(
    private val cleanAPKRepository: CleanAPKRepository,
@@ -76,6 +84,9 @@ class FusedAPIImpl @Inject constructor(
    private val pwaManagerModule: PWAManagerModule,
    private val preferenceManagerModule: PreferenceManagerModule,
    private val fdroidWebInterface: FdroidWebInterface,
    @Named("gplayRepository") private val gplayRepository: StoreRepository,
    @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: StoreRepository,
    @Named("cleanApkPWARepository") private val cleanApkPWARepository: StoreRepository,
    @ApplicationContext private val context: Context
) {

@@ -152,20 +163,16 @@ class FusedAPIImpl @Inject constructor(
            })

            Source.OPEN -> runCodeBlockWithTimeout({
                val response = cleanAPKRepository.getHomeScreenData(
                    CleanAPKInterface.APP_TYPE_ANY,
                    CleanAPKInterface.APP_SOURCE_FOSS
                ).body()
                val response =
                    (cleanApkAppsRepository.getHomeScreenData() as Response<HomeScreen>).body()
                response?.home?.let {
                    priorList.addAll(generateCleanAPKHome(it, APP_TYPE_OPEN))
                }
            })

            Source.PWA -> runCodeBlockWithTimeout({
                val response = cleanAPKRepository.getHomeScreenData(
                    CleanAPKInterface.APP_TYPE_PWA,
                    CleanAPKInterface.APP_SOURCE_ANY
                ).body()
                val response =
                    (cleanApkPWARepository.getHomeScreenData() as Response<HomeScreen>).body()
                response?.home?.let {
                    priorList.addAll(generateCleanAPKHome(it, APP_TYPE_PWA))
                }
@@ -272,7 +279,7 @@ class FusedAPIImpl @Inject constructor(
                        authData,
                        searchResult,
                        packageSpecificResults
                    )
                    ).asLiveData()
                )
            }
        }
@@ -286,11 +293,9 @@ class FusedAPIImpl @Inject constructor(
    ): ResultSupreme<Pair<List<FusedApp>, Boolean>> {
        val pwaApps: MutableList<FusedApp> = mutableListOf()
        val status = fusedAPIImpl.runCodeBlockWithTimeout({
            getCleanAPKSearchResults(
                query,
                CleanAPKInterface.APP_SOURCE_ANY,
                CleanAPKInterface.APP_TYPE_PWA
            ).apply {
            val apps =
                (cleanApkPWARepository.getSearchResult(query) as Response<Search>).body()?.apps
            apps?.apply {
                if (this.isNotEmpty()) {
                    pwaApps.addAll(this)
                }
@@ -314,13 +319,12 @@ class FusedAPIImpl @Inject constructor(
        )
    }

    private fun fetchGplaySearchResults(
    private suspend fun fetchGplaySearchResults(
        query: String,
        authData: AuthData,
        searchResult: MutableList<FusedApp>,
        packageSpecificResults: ArrayList<FusedApp>
    ): LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> =
        getGplaySearchResults(query, authData).map {
    ): GplaySearchResultFlow = getGplaySearchResult(query, authData).map {
        if (it.first.isNotEmpty()) {
            searchResult.addAll(it.first)
        }
@@ -469,7 +473,7 @@ class FusedAPIImpl @Inject constructor(
    }

    suspend fun getSearchSuggestions(query: String, authData: AuthData): List<SearchSuggestEntry> {
        return gPlayAPIRepository.getSearchSuggestions(query, authData)
        return gplayRepository.getSearchSuggestions(query) as List<SearchSuggestEntry>
    }

    suspend fun getOnDemandModule(
@@ -1174,7 +1178,7 @@ class FusedAPIImpl @Inject constructor(
    ): List<FusedApp> {
        val list = mutableListOf<FusedApp>()
        val response =
            cleanAPKRepository.searchApps(keyword, source, type, nres, page, by).body()?.apps
            (cleanApkAppsRepository.getSearchResult(keyword) as Response<Search>).body()?.apps

        response?.forEach {
            it.updateStatus()
@@ -1190,7 +1194,8 @@ class FusedAPIImpl @Inject constructor(
        query: String,
        authData: AuthData
    ): LiveData<Pair<List<FusedApp>, Boolean>> {
        val searchResults = gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid)
        val searchResults =
            gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid)
        return searchResults.map {
            Pair(
                it.first,
@@ -1199,6 +1204,20 @@ class FusedAPIImpl @Inject constructor(
        }
    }

    private suspend fun getGplaySearchResult(
        query: String,
        authData: AuthData
    ): Flow<Pair<List<FusedApp>, Boolean>> {
        val searchResults = gplayRepository.getSearchResult(query) as Flow<Pair<List<App>, Boolean>>
        return searchResults.map {
            val fusedAppList = it.first.map { app -> replaceWithFDroid(app) }
            Pair(
                fusedAppList,
                it.second
            )
        }
    }

    /*
         * This function will replace a GPlay app with F-Droid app if exists,
         * else will show the GPlay app itself.
@@ -1320,24 +1339,16 @@ class FusedAPIImpl @Inject constructor(

    private suspend fun fetchGPlayHome(authData: AuthData): List<FusedHome> {
        val list = mutableListOf<FusedHome>()
        val homeElements = mutableMapOf(
            context.getString(R.string.topselling_free_apps) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION),
            context.getString(R.string.topselling_free_games) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME),
            context.getString(R.string.topgrossing_apps) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION),
            context.getString(R.string.topgrossing_games) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.GAME),
            context.getString(R.string.movers_shakers_apps) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION),
            context.getString(R.string.movers_shakers_games) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME),
        )
        homeElements.forEach {
            val chart = it.value.keys.iterator().next()
            val type = it.value.values.iterator().next()
            val result = gPlayAPIRepository.getTopApps(type, chart, authData).map { app ->
        val gplayHomeData = gplayRepository.getHomeScreenData() as Map<String, List<App>>
        gplayHomeData.map {
            val fusedApps = it.value.map { app ->
                app.transformToFusedApp().apply {
                    updateFilterLevel(authData)
                }
            }
            list.add(FusedHome(it.key, result))
            list.add(FusedHome(it.key, fusedApps))
        }
        Timber.d("===> $list")
        return list
    }

+206 −0
Original line number Diff line number Diff line
/*
 * Copyright MURENA SAS 2023
 * Apps  Quickly and easily install Android apps onto your device!
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.apps.api.gplay

import android.content.Context
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.SearchBundle
import com.aurora.gplayapi.helpers.SearchHelper
import com.aurora.gplayapi.helpers.TopChartsHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.api.StoreRepository
import foundation.e.apps.api.gplay.utils.GPlayHttpClient
import foundation.e.apps.login.LoginSourceRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import javax.inject.Inject

class GplayRepository @Inject constructor(
    @ApplicationContext private val context: Context,
    private val gPlayHttpClient: GPlayHttpClient,
    private val loginSourceRepository: LoginSourceRepository
) : StoreRepository {

    override suspend fun getHomeScreenData(): Any {
        val homeScreenData = mutableMapOf<String, List<App>>()
        val homeElements = createTopChartElements()

        homeElements.forEach {
            val chart = it.value.keys.iterator().next()
            val type = it.value.values.iterator().next()
            val result = getTopApps(type, chart, loginSourceRepository.gplayAuth!!)
            homeScreenData[it.key] = result
        }

        return homeScreenData
    }

    private fun createTopChartElements() = mutableMapOf(
        context.getString(R.string.topselling_free_apps) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION),
        context.getString(R.string.topselling_free_games) to mapOf(TopChartsHelper.Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME),
        context.getString(R.string.topgrossing_apps) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION),
        context.getString(R.string.topgrossing_games) to mapOf(TopChartsHelper.Chart.TOP_GROSSING to TopChartsHelper.Type.GAME),
        context.getString(R.string.movers_shakers_apps) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION),
        context.getString(R.string.movers_shakers_games) to mapOf(TopChartsHelper.Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME),
    )

    override suspend fun getSearchResult(query: String): Flow<Pair<List<App>, Boolean>> {
        return flow {
            /*
             * Variable names and logic made same as that of Aurora store.
             * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171
             */
            val searchHelper =
                SearchHelper(loginSourceRepository.gplayAuth!!).using(gPlayHttpClient)
            val searchBundle = searchHelper.searchResults(query)

            val initialReplacedList = mutableListOf<App>()
            val INITIAL_LIMIT = 4

            emitReplacedList(
                this@flow,
                initialReplacedList,
                INITIAL_LIMIT,
                searchBundle,
                true,
            )

            var nextSubBundleSet: MutableSet<SearchBundle.SubBundle>
            do {
                nextSubBundleSet = fetchNextSubBundle(
                    searchBundle,
                    searchHelper,
                    this@flow,
                    initialReplacedList,
                    INITIAL_LIMIT
                )
            } while (nextSubBundleSet.isNotEmpty())

            /*
             * If initialReplacedList size is less than INITIAL_LIMIT,
             * it means the results were very less and nothing has been emitted so far.
             * Hence emit the list.
             */
            if (initialReplacedList.size < INITIAL_LIMIT) {
                emitInMain(this@flow, initialReplacedList, false)
            }
        }.flowOn(Dispatchers.IO)
    }

    private suspend fun fetchNextSubBundle(
        searchBundle: SearchBundle,
        searchHelper: SearchHelper,
        scope: FlowCollector<Pair<List<App>, Boolean>>,
        accumulationList: MutableList<App>,
        accumulationLimit: Int,
    ): MutableSet<SearchBundle.SubBundle> {
        val nextSubBundleSet = searchBundle.subBundles
        val newSearchBundle = searchHelper.next(nextSubBundleSet)
        if (newSearchBundle.appList.isNotEmpty()) {
            searchBundle.apply {
                subBundles.clear()
                subBundles.addAll(newSearchBundle.subBundles)
                emitReplacedList(
                    scope,
                    accumulationList,
                    accumulationLimit,
                    newSearchBundle,
                    nextSubBundleSet.isNotEmpty(),
                )
            }
        }
        return nextSubBundleSet
    }

    override suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry> {
        val searchData = mutableListOf<SearchSuggestEntry>()
        withContext(Dispatchers.IO) {
            val searchHelper =
                SearchHelper(loginSourceRepository.gplayAuth!!).using(gPlayHttpClient)
            searchData.addAll(searchHelper.searchSuggestions(query))
        }
        return searchData.filter { it.suggestedQuery.isNotBlank() }
    }

    private suspend fun emitReplacedList(
        scope: FlowCollector<Pair<List<App>, Boolean>>,
        accumulationList: MutableList<App>,
        accumulationLimit: Int,
        searchBundle: SearchBundle,
        moreToEmit: Boolean,
    ) {
        searchBundle.appList.forEach {
            when {
                accumulationList.size < accumulationLimit - 1 -> {
                    /*
                     * If initial limit is 4, add apps to list (without emitting)
                     * till 2 apps.
                     */
                    accumulationList.add(it)
                }

                accumulationList.size == accumulationLimit - 1 -> {
                    /*
                     * If initial limit is 4, and we have reached till 3 apps,
                     * add the 4th app and emit the list.
                     */
                    accumulationList.add(it)
                    scope.emit(Pair(accumulationList, moreToEmit))
                    emitInMain(scope, accumulationList, moreToEmit)
                }

                accumulationList.size == accumulationLimit -> {
                    /*
                     * If initial limit is 4, and we have emitted 4 apps,
                     * for all rest of the apps, emit each app one by one.
                     */
                    emitInMain(scope, listOf(it), moreToEmit)
                }
            }
        }
    }

    private suspend fun emitInMain(
        scope: FlowCollector<Pair<List<App>, Boolean>>,
        it: List<App>,
        moreToEmit: Boolean
    ) {
        scope.emit(Pair(it, moreToEmit))
    }

    private suspend fun getTopApps(
        type: TopChartsHelper.Type,
        chart: TopChartsHelper.Chart,
        authData: AuthData
    ): List<App> {
        val topApps = mutableListOf<App>()
        withContext(Dispatchers.IO) {
            val topChartsHelper = TopChartsHelper(authData).using(gPlayHttpClient)
            topApps.addAll(topChartsHelper.getCluster(type, chart).clusterAppList)
        }
        return topApps
    }
}
Loading