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

Commit 8acbda98 authored by Hasib Prince's avatar Hasib Prince
Browse files

Merge branch '1709-apps_api' into 'main'

Do not count private methods in detekt

See merge request !403
parents 7f15e8ec 82ee4695
Loading
Loading
Loading
Loading
Loading
+0 −67
Original line number Diff line number Diff line
@@ -62,71 +62,4 @@ interface ApplicationApi {

    suspend fun getOSSDownloadInfo(id: String, version: String?): Response<Download>

    /*
        * 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<Application, ResultStatus>

    suspend fun getApplicationDetails(
        packageNameList: List<String>,
        authData: AuthData,
        origin: Origin
    ): Pair<List<Application>, ResultStatus>

    /**
     * Filter out apps which are restricted, whose details cannot be fetched.
     * If an app is restricted, we do try to fetch the app details inside a
     * try-catch block. If that fails, we remove the app, else we keep it even
     * if it is restricted.
     *
     * Popular example: "com.skype.m2"
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     */
    suspend fun filterRestrictedGPlayApps(
        authData: AuthData,
        appList: List<App>,
    ): ResultSupreme<List<Application>>

    /**
     * Get different filter levels.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720
     */
    suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel

    /*
        * Similar to above method but uses Aurora OSS data class "App".
        */
    suspend fun getAppFilterLevel(app: App, authData: AuthData): FilterLevel

    suspend fun getApplicationDetails(
        id: String,
        packageName: String,
        authData: AuthData,
        origin: Origin
    ): Pair<Application, ResultStatus>

    /**
     * Get fused app installation status.
     * Applicable for both native apps and PWAs.
     *
     * Recommended to use this instead of [PkgManagerModule.getPackageStatus].
     */
    fun getFusedAppInstallationStatus(application: Application): Status

    /**
     * @return returns true if there is changes in data, otherwise false
     */
    fun isAnyFusedAppUpdated(
        newApplications: List<Application>,
        oldApplications: List<Application>
    ): Boolean

    fun isAnyAppInstallStatusChanged(currentList: List<Application>): Boolean
    fun isOpenSourceSelected(): Boolean

}
+13 −399
Original line number Diff line number Diff line
@@ -19,71 +19,52 @@
package foundation.e.apps.data.application

import android.content.Context
import android.text.format.Formatter
import com.aurora.gplayapi.Constants
import com.aurora.gplayapi.SearchSuggestEntry
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.SearchBundle
import com.aurora.gplayapi.data.models.StreamCluster
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.R
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_ANY
import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_OPEN
import foundation.e.apps.data.application.ApplicationApi.Companion.APP_TYPE_PWA
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Category
import foundation.e.apps.data.application.data.Home
import foundation.e.apps.data.application.data.Ratings
import foundation.e.apps.data.application.utils.CategoryType
import foundation.e.apps.data.application.utils.CategoryUtils
import foundation.e.apps.data.application.utils.toApplication
import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher
import foundation.e.apps.data.cleanapk.data.categories.Categories
import foundation.e.apps.data.cleanapk.data.search.Search
import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository
import foundation.e.apps.data.enums.AppTag
import foundation.e.apps.data.enums.FilterLevel
import foundation.e.apps.data.enums.Origin
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.enums.isUnFiltered
import foundation.e.apps.data.fusedDownload.models.FusedDownload
import foundation.e.apps.data.handleNetworkResult
import foundation.e.apps.data.login.AuthObject
import foundation.e.apps.data.playstore.PlayStoreRepository
import foundation.e.apps.data.preference.PreferenceManagerModule
import foundation.e.apps.install.pkg.PWAManagerModule
import foundation.e.apps.install.pkg.PkgManagerModule
import foundation.e.apps.ui.applicationlist.ApplicationDiffUtil
import foundation.e.apps.utils.eventBus.AppEvent
import foundation.e.apps.utils.eventBus.EventBus
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import retrofit2.Response
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
import com.aurora.gplayapi.data.models.Category as GplayapiCategory
import foundation.e.apps.data.cleanapk.data.app.Application as CleanApkApplication

typealias FusedHomeDeferred = Deferred<ResultSupreme<List<Home>>>

@Singleton
class ApplicationApiImpl @Inject constructor(
    private val pkgManagerModule: PkgManagerModule,
    private val pwaManagerModule: PWAManagerModule,
    private val appsApi: AppsApi,
    private val preferenceManagerModule: PreferenceManagerModule,
    @Named("gplayRepository") private val gplayRepository: PlayStoreRepository,
    @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: CleanApkRepository,
    @Named("cleanApkPWARepository") private val cleanApkPWARepository: CleanApkRepository,
    @ApplicationContext private val context: Context
    private val applicationDataManager: ApplicationDataManager
) : ApplicationApi {

    @Inject
    @ApplicationContext lateinit var context: Context

    companion object {
        private const val KEYWORD_TEST_SEARCH = "facebook"
    }
@@ -108,11 +89,6 @@ class ApplicationApiImpl @Inject constructor(
        query: String,
        authData: AuthData
    ): ResultSupreme<Pair<List<Application>, 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
         */
        val packageSpecificResults = ArrayList<Application>()
        var finalSearchResult: ResultSupreme<Pair<List<Application>, Boolean>> = ResultSupreme.Error()

@@ -150,9 +126,9 @@ class ApplicationApiImpl @Inject constructor(
            val apps =
                cleanApkPWARepository.getSearchResult(query).body()?.apps
            apps?.forEach {
                it.updateStatus()
                applicationDataManager.updateStatus(it)
                it.updateType()
                it.updateSource()
                it.updateSource(context)
                pwaApps.add(it)
            }
        }
@@ -287,7 +263,7 @@ class ApplicationApiImpl @Inject constructor(
        authData: AuthData,
    ): Application? {
        try {
            getApplicationDetails(query, query, authData, Origin.GPLAY).let {
            appsApi.getApplicationDetails(query, query, authData, Origin.GPLAY).let {
                if (it.second == ResultStatus.OK && it.first.package_name.isNotEmpty()) {
                    return it.first
                }
@@ -383,252 +359,6 @@ class ApplicationApiImpl @Inject constructor(
    override suspend fun getOSSDownloadInfo(id: String, version: String?) =
        (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo(id, version)

    /*
     * Function to search cleanapk using package name.
     * Will be used to handle f-droid deeplink.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509
     */
    override suspend fun getCleanapkAppDetails(packageName: String): Pair<Application, ResultStatus> {
        var application = Application()
        val result = handleNetworkResult {
            val result = cleanApkAppsRepository.getSearchResult(
                packageName,
                "package_name"
            ).body()

            if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) {
                application =
                    (cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response<CleanApkApplication>).body()?.app
                        ?: Application()
            }
            application.updateFilterLevel(null)
        }
        return Pair(application, result.getResultStatus())
    }

    // Warning - GPlay results may not have proper geo-restriction information.
    override suspend fun getApplicationDetails(
        packageNameList: List<String>,
        authData: AuthData,
        origin: Origin
    ): Pair<List<Application>, ResultStatus> {
        val list = mutableListOf<Application>()

        val response: Pair<List<Application>, ResultStatus> =
            if (origin == Origin.CLEANAPK) {
                getAppDetailsListFromCleanapk(packageNameList)
            } else {
                getAppDetailsListFromGPlay(packageNameList, authData)
            }

        response.first.forEach {
            if (it.package_name.isNotBlank()) {
                it.updateStatus()
                it.updateType()
                list.add(it)
            }
        }

        return Pair(list, response.second)
    }

    /*
     * Get app details of a list of apps from cleanapk.
     * Returns list of FusedApp and ResultStatus - which will reflect timeout if even one app fails.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413
     */
    private suspend fun getAppDetailsListFromCleanapk(
        packageNameList: List<String>,
    ): Pair<List<Application>, ResultStatus> {
        var status = ResultStatus.OK
        val applicationList = mutableListOf<Application>()

        /*
         * Fetch result of each cleanapk search with separate timeout,
         * i.e. check timeout for individual package query.
         */
        for (packageName in packageNameList) {
            val result = handleNetworkResult {
                cleanApkAppsRepository.getSearchResult(
                    packageName,
                    "package_name"
                ).body()?.run {
                    if (apps.isNotEmpty() && numberOfResults == 1) {
                        applicationList.add(
                            apps[0].apply {
                                updateFilterLevel(null)
                            }
                        )
                    }
                }
            }

            status = result.getResultStatus()

            /*
             * If status is not ok, immediately return.
             */
            if (status != ResultStatus.OK) {
                return Pair(applicationList, status)
            }
        }

        return Pair(applicationList, status)
    }

    /*
     * Get app details of a list of apps from Google Play store.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413
     */
    private suspend fun getAppDetailsListFromGPlay(
        packageNameList: List<String>,
        authData: AuthData,
    ): Pair<List<Application>, ResultStatus> {
        val applicationList = mutableListOf<Application>()

        /*
         * Old code moved from getApplicationDetails()
         */
        val result = handleNetworkResult {
            gplayRepository.getAppsDetails(packageNameList).forEach { app ->
                /*
                 * Some apps are restricted to locations. Example "com.skype.m2".
                 * For restricted apps, check if it is possible to get their specific app info.
                 *
                 * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174
                 */
                val filter = getAppFilterLevel(app, authData)
                if (filter.isUnFiltered()) {
                    applicationList.add(
                        app.transformToFusedApp().apply {
                            filterLevel = filter
                        }
                    )
                }
            }
        }

        return Pair(applicationList, result.getResultStatus())
    }

    /**
     * Filter out apps which are restricted, whose details cannot be fetched.
     * If an app is restricted, we do try to fetch the app details inside a
     * try-catch block. If that fails, we remove the app, else we keep it even
     * if it is restricted.
     *
     * Popular example: "com.skype.m2"
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5174
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2]
     */
    override suspend fun filterRestrictedGPlayApps(
        authData: AuthData,
        appList: List<App>,
    ): ResultSupreme<List<Application>> {
        val filteredApplications = mutableListOf<Application>()
        return handleNetworkResult {
            appList.forEach {
                val filter = getAppFilterLevel(it, authData)
                if (filter.isUnFiltered()) {
                    filteredApplications.add(
                        it.transformToFusedApp().apply {
                            this.filterLevel = filter
                        }
                    )
                }
            }
            filteredApplications
        }
    }

    /**
     * Get different filter levels.
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5720
     */
    override suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel {
        return when {
            application.package_name.isBlank() -> FilterLevel.UNKNOWN
            !application.isFree && application.price.isBlank() -> FilterLevel.UI
            application.origin == Origin.CLEANAPK -> FilterLevel.NONE
            !isRestricted(application) -> FilterLevel.NONE
            authData == null -> FilterLevel.UNKNOWN // cannot determine for gplay app
            !isApplicationVisible(application) -> FilterLevel.DATA
            application.originalSize == 0L -> FilterLevel.UI
            !isDownloadable(application) -> FilterLevel.UI
            else -> FilterLevel.NONE
        }
    }

    /**
     * Some apps are simply not visible.
     * Example: com.skype.m2
     */
    private suspend fun isApplicationVisible(application: Application): Boolean {
        return kotlin.runCatching { gplayRepository.getAppDetails(application.package_name) }.isSuccess
    }

    /**
     * Some apps are visible but not downloadable.
     * Example: com.riotgames.league.wildrift
     */
    private suspend fun isDownloadable(application: Application): Boolean {
        return kotlin.runCatching {
            gplayRepository.getDownloadInfo(
                application.package_name,
                application.latest_version_code,
                application.offer_type,
            )
        }.isSuccess
    }

    private fun isRestricted(application: Application): Boolean {
        return application.restriction != Constants.Restriction.NOT_RESTRICTED
    }

    /*
     * Similar to above method but uses Aurora OSS data class "App".
     */
    override suspend fun getAppFilterLevel(app: App, authData: AuthData): FilterLevel {
        return getAppFilterLevel(app.transformToFusedApp(), authData)
    }

    /*
     * Handy method to run on an instance of FusedApp to update its filter level.
     */
    suspend fun Application.updateFilterLevel(authData: AuthData?) {
        this.filterLevel = getAppFilterLevel(this, authData)
    }

    override suspend fun getApplicationDetails(
        id: String,
        packageName: String,
        authData: AuthData,
        origin: Origin
    ): Pair<Application, ResultStatus> {

        var response: Application? = null

        val result = handleNetworkResult {
            response = if (origin == Origin.CLEANAPK) {
                (cleanApkAppsRepository.getAppDetails(id) as Response<CleanApkApplication>).body()?.app
            } else {
                val app = gplayRepository.getAppDetails(packageName) as App?
                app?.transformToFusedApp()
            }
            response?.let {
                it.updateStatus()
                it.updateType()
                it.updateSource()
                it.updateFilterLevel(authData)
            }
            response
        }

        return Pair(result.data ?: Application(), result.getResultStatus())
    }

    /*
     * Search-related internal functions
     */
@@ -641,9 +371,9 @@ class ApplicationApiImpl @Inject constructor(
            cleanApkAppsRepository.getSearchResult(keyword).body()?.apps

        response?.forEach {
            it.updateStatus()
            applicationDataManager.updateStatus(it)
            it.updateType()
            it.updateSource()
            it.updateSource(context)
            list.add(it)
        }
        return list
@@ -691,11 +421,11 @@ class ApplicationApiImpl @Inject constructor(
         * else will show the GPlay app itself.
         */
    private suspend fun replaceWithFDroid(gPlayApp: App): Application {
        val gPlayFusedApp = gPlayApp.transformToFusedApp()
        val gPlayFusedApp = gPlayApp.toApplication(context)
        val response = cleanApkAppsRepository.getAppDetails(gPlayApp.packageName)
        if (response != null) {
            val fdroidApp = getCleanApkPackageResult(gPlayFusedApp.package_name)?.apply {
                updateSource()
                this.updateSource(context)
                isGplayReplaced = true
            }
            return fdroidApp ?: gPlayFusedApp
@@ -711,120 +441,4 @@ class ApplicationApiImpl @Inject constructor(
            )
        }
    }

    /*
     * FusedApp-related internal extensions and functions
     */

    private fun App.transformToFusedApp(): Application {
        val app = Application(
            _id = this.id.toString(),
            author = this.developerName,
            category = this.categoryName,
            description = this.description,
            perms = this.permissions,
            icon_image_path = this.iconArtwork.url,
            last_modified = this.updatedOn,
            latest_version_code = this.versionCode,
            latest_version_number = this.versionName,
            name = this.displayName,
            other_images_path = this.screenshots.transformToList(),
            package_name = this.packageName,
            ratings = Ratings(
                usageQualityScore =
                this.labeledRating.run {
                    if (isNotEmpty()) {
                        this.replace(",", ".").toDoubleOrNull() ?: -1.0
                    } else -1.0
                }
            ),
            offer_type = this.offerType,
            origin = Origin.GPLAY,
            shareUrl = this.shareUrl,
            originalSize = this.size,
            appSize = Formatter.formatFileSize(context, this.size),
            isFree = this.isFree,
            price = this.price,
            restriction = this.restriction,
        )
        app.updateStatus()
        return app
    }

    /**
     * Get fused app installation status.
     * Applicable for both native apps and PWAs.
     *
     * Recommended to use this instead of [PkgManagerModule.getPackageStatus].
     */
    override fun getFusedAppInstallationStatus(application: Application): Status {
        return if (application.is_pwa) {
            pwaManagerModule.getPwaStatus(application)
        } else {
            pkgManagerModule.getPackageStatus(application.package_name, application.latest_version_code)
        }
    }

    private fun Application.updateStatus() {
        if (this.status != Status.INSTALLATION_ISSUE) {
            this.status = getFusedAppInstallationStatus(this)
        }
    }

    private fun Application.updateType() {
        this.type = if (this.is_pwa) Type.PWA else Type.NATIVE
    }

    private fun Application.updateSource() {
        this.apply {
            source = if (origin == Origin.CLEANAPK && is_pwa) context.getString(R.string.pwa)
            else if (origin == Origin.CLEANAPK) context.getString(R.string.open_source)
            else ""
        }
    }

    private fun MutableList<Artwork>.transformToList(): List<String> {
        val list = mutableListOf<String>()
        this.forEach {
            list.add(it.url)
        }
        return list
    }

    /**
     * @return returns true if there is changes in data, otherwise false
     */
    override fun isAnyFusedAppUpdated(
        newApplications: List<Application>,
        oldApplications: List<Application>
    ): Boolean {
        val fusedAppDiffUtil = ApplicationDiffUtil()
        if (newApplications.size != oldApplications.size) {
            return true
        }

        newApplications.forEach {
            val indexOfNewFusedApp = newApplications.indexOf(it)
            if (!fusedAppDiffUtil.areContentsTheSame(it, oldApplications[indexOfNewFusedApp])) {
                return true
            }
        }
        return false
    }

    override fun isAnyAppInstallStatusChanged(currentList: List<Application>): Boolean {
        currentList.forEach {
            if (it.status == Status.INSTALLATION_ISSUE) {
                return@forEach
            }
            val currentAppStatus =
                getFusedAppInstallationStatus(it)
            if (it.status != currentAppStatus) {
                return true
            }
        }
        return false
    }

    override fun isOpenSourceSelected() = preferenceManagerModule.isOpenSourceSelected()
}
+10 −9
Original line number Diff line number Diff line
@@ -40,7 +40,8 @@ import javax.inject.Singleton
class ApplicationRepository @Inject constructor(
    private val applicationAPIImpl: ApplicationApi,
    private val homeApi: HomeApi,
    private val categoryApi: CategoryApi
    private val categoryApi: CategoryApi,
    private val appsApi: AppsApi,
) {

    suspend fun getHomeScreenData(authData: AuthData): LiveData<ResultSupreme<List<Home>>> {
@@ -56,11 +57,11 @@ class ApplicationRepository @Inject constructor(
        authData: AuthData,
        origin: Origin
    ): Pair<List<Application>, ResultStatus> {
        return applicationAPIImpl.getApplicationDetails(packageNameList, authData, origin)
        return appsApi.getApplicationDetails(packageNameList, authData, origin)
    }

    suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel {
        return applicationAPIImpl.getAppFilterLevel(application, authData)
        return appsApi.getAppFilterLevel(application, authData)
    }

    suspend fun getApplicationDetails(
@@ -69,11 +70,11 @@ class ApplicationRepository @Inject constructor(
        authData: AuthData,
        origin: Origin
    ): Pair<Application, ResultStatus> {
        return applicationAPIImpl.getApplicationDetails(id, packageName, authData, origin)
        return appsApi.getApplicationDetails(id, packageName, authData, origin)
    }

    suspend fun getCleanapkAppDetails(packageName: String): Pair<Application, ResultStatus> {
        return applicationAPIImpl.getCleanapkAppDetails(packageName)
        return appsApi.getCleanapkAppDetails(packageName)
    }

    suspend fun updateFusedDownloadWithDownloadingInfo(
@@ -136,16 +137,16 @@ class ApplicationRepository @Inject constructor(
    }

    fun getFusedAppInstallationStatus(application: Application): Status {
        return applicationAPIImpl.getFusedAppInstallationStatus(application)
        return appsApi.getFusedAppInstallationStatus(application)
    }

    fun isAnyFusedAppUpdated(
        newApplications: List<Application>,
        oldApplications: List<Application>
    ) = applicationAPIImpl.isAnyFusedAppUpdated(newApplications, oldApplications)
    ) = appsApi.isAnyFusedAppUpdated(newApplications, oldApplications)

    fun isAnyAppInstallStatusChanged(currentList: List<Application>) =
        applicationAPIImpl.isAnyAppInstallStatusChanged(currentList)
        appsApi.isAnyAppInstallStatusChanged(currentList)

    fun isOpenSourceSelected() = applicationAPIImpl.isOpenSourceSelected()
    fun isOpenSourceSelected() = appsApi.isOpenSourceSelected()
}
+69 −0

File added.

Preview size limit exceeded, changes collapsed.

+274 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading