diff --git a/app/src/main/java/foundation/e/apps/api/BaseStoreRepository.kt b/app/src/main/java/foundation/e/apps/api/BaseStoreRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..d72e5b37995b3fb8a730993d631ec5578d5051db --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/BaseStoreRepository.kt @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +package foundation.e.apps.api + +interface BaseStoreRepository { + suspend fun getHomeScreenData(): Any + suspend fun getAppDetails(packageNameOrId: String): Any? +} diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt index 45c47a58dca7c91ba0385fa0215ea1b55e563b1d..fc77fbb13ebbdf5cd68187d6183fb072c021b570 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt @@ -29,15 +29,15 @@ import javax.inject.Inject @OpenForTesting class CleanAPKRepository @Inject constructor( - private val cleanAPKInterface: CleanAPKInterface, - private val cleanApkAppDetailApi: CleanApkAppDetailApi + private val cleanAPKRetrofit: CleanApkRetrofit, + private val cleanApkAppDetailsRetrofit: CleanApkAppDetailsRetrofit ) { suspend fun getHomeScreenData( - type: String = CleanAPKInterface.APP_TYPE_ANY, - source: String = CleanAPKInterface.APP_SOURCE_ANY + type: String = CleanApkRetrofit.APP_TYPE_ANY, + source: String = CleanApkRetrofit.APP_SOURCE_ANY ): Response { - return cleanAPKInterface.getHomeScreenData(type, source) + return cleanAPKRetrofit.getHomeScreenData(type, source) } suspend fun getAppOrPWADetailsByID( @@ -45,28 +45,28 @@ class CleanAPKRepository @Inject constructor( architectures: List? = null, type: String? = null ): Response { - return cleanApkAppDetailApi.getAppOrPWADetailsByID(id, architectures, type) + return cleanApkAppDetailsRetrofit.getAppOrPWADetailsByID(id, architectures, type) } suspend fun searchApps( keyword: String, - source: String = CleanAPKInterface.APP_SOURCE_FOSS, - type: String = CleanAPKInterface.APP_TYPE_ANY, + source: String = CleanApkRetrofit.APP_SOURCE_FOSS, + type: String = CleanApkRetrofit.APP_TYPE_ANY, nres: Int = 20, page: Int = 1, by: String? = null ): Response { - return cleanAPKInterface.searchApps(keyword, source, type, nres, page, by) + return cleanAPKRetrofit.searchApps(keyword, source, type, nres, page, by) } suspend fun listApps( category: String, - source: String = CleanAPKInterface.APP_SOURCE_FOSS, - type: String = CleanAPKInterface.APP_TYPE_ANY, + source: String = CleanApkRetrofit.APP_SOURCE_FOSS, + type: String = CleanApkRetrofit.APP_TYPE_ANY, nres: Int = 20, page: Int = 1, ): Response { - return cleanAPKInterface.listApps(category, source, type, nres, page) + return cleanAPKRetrofit.listApps(category, source, type, nres, page) } suspend fun getDownloadInfo( @@ -74,13 +74,13 @@ class CleanAPKRepository @Inject constructor( version: String? = null, architecture: String? = null ): Response { - return cleanAPKInterface.getDownloadInfo(id, version, architecture) + return cleanAPKRetrofit.getDownloadInfo(id, version, architecture) } suspend fun getCategoriesList( - type: String = CleanAPKInterface.APP_TYPE_ANY, - source: String = CleanAPKInterface.APP_SOURCE_ANY + type: String = CleanApkRetrofit.APP_TYPE_ANY, + source: String = CleanApkRetrofit.APP_SOURCE_ANY ): Response { - return cleanAPKInterface.getCategoriesList(type, source) + return cleanAPKRetrofit.getCategoriesList(type, source) } } diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppDetailApi.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppDetailsRetrofit.kt similarity index 97% rename from app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppDetailApi.kt rename to app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppDetailsRetrofit.kt index f6795e12ca59fd8cc3521d5e134755eb44e3cc5e..9ab5b6f479ee78a694a91c17cdb00e65abb86435 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppDetailApi.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppDetailsRetrofit.kt @@ -25,7 +25,7 @@ import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query -interface CleanApkAppDetailApi { +interface CleanApkAppDetailsRetrofit { companion object { // API endpoints diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppsRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppsRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..4fc16cbf3f419df2790719725d4353b2251fbcff --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkAppsRepositoryImpl.kt @@ -0,0 +1,79 @@ +/* + * 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 . + */ + +package foundation.e.apps.api.cleanapk + +import foundation.e.apps.api.cleanapk.data.app.Application +import foundation.e.apps.api.cleanapk.data.categories.Categories +import foundation.e.apps.api.cleanapk.data.download.Download +import foundation.e.apps.api.cleanapk.data.home.HomeScreen +import foundation.e.apps.api.cleanapk.data.search.Search +import retrofit2.Response + +class CleanApkAppsRepositoryImpl( + private val cleanApkRetrofit: CleanApkRetrofit, + private val cleanApkAppDetailsRetrofit: CleanApkAppDetailsRetrofit +) : CleanApkRepository, CleanApkDownloadInfoFetcher { + + override suspend fun getHomeScreenData(): Response { + return cleanApkRetrofit.getHomeScreenData( + CleanApkRetrofit.APP_TYPE_ANY, + CleanApkRetrofit.APP_SOURCE_FOSS + ) + } + + override suspend fun getSearchResult(query: String, searchBy: String?): Response { + return cleanApkRetrofit.searchApps( + query, + CleanApkRetrofit.APP_SOURCE_FOSS, + CleanApkRetrofit.APP_TYPE_ANY, + NUMBER_OF_ITEMS, + NUMBER_OF_PAGES, + searchBy + ) + } + + override suspend fun getAppsByCategory( + category: String, + paginationParameter: Any? + ): Response { + return cleanApkRetrofit.listApps( + category, + CleanApkRetrofit.APP_SOURCE_FOSS, + CleanApkRetrofit.APP_TYPE_ANY, + NUMBER_OF_ITEMS, + NUMBER_OF_PAGES + ) + } + + override suspend fun getCategories(): Response { + return cleanApkRetrofit.getCategoriesList( + CleanApkRetrofit.APP_TYPE_ANY, + CleanApkRetrofit.APP_SOURCE_FOSS + ) + } + + override suspend fun getAppDetails(packageNameOrId: String): Response { + return cleanApkAppDetailsRetrofit.getAppOrPWADetailsByID(packageNameOrId, null, null) + } + + override suspend fun getDownloadInfo(idOrPackageName: String, versionCode: Any?): Response { + val version = versionCode?.let { it as String } + return cleanApkRetrofit.getDownloadInfo(idOrPackageName, version, null) + } +} diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkDownloadInfoFetcher.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkDownloadInfoFetcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..14dcd7f6ce0332accc74608f04155390a4f3a03c --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkDownloadInfoFetcher.kt @@ -0,0 +1,26 @@ +/* + * 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 . + */ + +package foundation.e.apps.api.cleanapk + +import foundation.e.apps.api.cleanapk.data.download.Download +import retrofit2.Response + +interface CleanApkDownloadInfoFetcher { + suspend fun getDownloadInfo(idOrPackageName: String, versionCode: Any? = null): Response +} diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkPWARepository.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkPWARepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..0bb4afa657fdfb5865a71af8aa3de6d3d71ecb47 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkPWARepository.kt @@ -0,0 +1,69 @@ +/* + * 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 . + */ + +package foundation.e.apps.api.cleanapk + +import foundation.e.apps.api.cleanapk.data.app.Application +import foundation.e.apps.api.cleanapk.data.categories.Categories +import foundation.e.apps.api.cleanapk.data.search.Search +import retrofit2.Response + +class CleanApkPWARepository( + private val cleanAPKRetrofit: CleanApkRetrofit, + private val cleanApkAppDetailsRetrofit: CleanApkAppDetailsRetrofit +) : CleanApkRepository { + + override suspend fun getHomeScreenData(): Any { + return cleanAPKRetrofit.getHomeScreenData( + CleanApkRetrofit.APP_TYPE_PWA, + CleanApkRetrofit.APP_SOURCE_ANY + ) + } + + override suspend fun getSearchResult(query: String, searchBy: String?): Response { + return cleanAPKRetrofit.searchApps( + query, + CleanApkRetrofit.APP_SOURCE_ANY, + CleanApkRetrofit.APP_TYPE_PWA, + 20, + 1, + searchBy + ) + } + + override suspend fun getAppsByCategory(category: String, paginationParameter: Any?): Response { + return cleanAPKRetrofit.listApps( + category, + CleanApkRetrofit.APP_SOURCE_ANY, + CleanApkRetrofit.APP_TYPE_PWA, + NUMBER_OF_ITEMS, + NUMBER_OF_PAGES + ) + } + + override suspend fun getCategories(): Response { + return cleanAPKRetrofit.getCategoriesList( + CleanApkRetrofit.APP_TYPE_PWA, + CleanApkRetrofit.APP_SOURCE_ANY + ) + } + + override suspend fun getAppDetails(packageNameOrId: String): Response { + return cleanApkAppDetailsRetrofit.getAppOrPWADetailsByID(packageNameOrId, null, null) + } +} diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkRepository.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..3959d18eb566eb3ea6f43cc04b4e5e4caa3c9875 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkRepository.kt @@ -0,0 +1,33 @@ +/* + * 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 . + */ + +package foundation.e.apps.api.cleanapk + +import foundation.e.apps.api.BaseStoreRepository +import foundation.e.apps.api.cleanapk.data.categories.Categories +import foundation.e.apps.api.cleanapk.data.search.Search +import retrofit2.Response + +const val NUMBER_OF_ITEMS = 20 + +const val NUMBER_OF_PAGES = 1 +interface CleanApkRepository : BaseStoreRepository { + suspend fun getSearchResult(query: String, searchBy: String? = null): Response + suspend fun getAppsByCategory(category: String, paginationParameter: Any? = null): Response + suspend fun getCategories(): Response +} diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKInterface.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkRetrofit.kt similarity index 96% rename from app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKInterface.kt rename to app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkRetrofit.kt index 6a95993359c5200242ac520d2e9becc174e978b1..4feb18bc54b1221f9f7c1ff966573b0a3efa3eb6 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKInterface.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanApkRetrofit.kt @@ -27,7 +27,7 @@ import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query -interface CleanAPKInterface { +interface CleanApkRetrofit { companion object { // API endpoints diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt index b03baa0b3916ed7aafc3bed8819b1403633fbd8a..26e8f8cad6e325789c91fff433f7289c6e904ef8 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt @@ -57,35 +57,35 @@ object RetrofitModule { /** * Provides an instance of Retrofit to work with CleanAPK API - * @return instance of [CleanAPKInterface] + * @return instance of [CleanApkRetrofit] */ @Singleton @Provides - fun provideCleanAPKInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanAPKInterface { + fun provideCleanAPKInterface(okHttpClient: OkHttpClient, moshi: Moshi): CleanApkRetrofit { return Retrofit.Builder() - .baseUrl(CleanAPKInterface.BASE_URL) + .baseUrl(CleanApkRetrofit.BASE_URL) .client(okHttpClient) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() - .create(CleanAPKInterface::class.java) + .create(CleanApkRetrofit::class.java) } /** * Provides an instance of Retrofit to work with CleanAPK API - * @return instance of [CleanApkAppDetailApi] + * @return instance of [CleanApkAppDetailsRetrofit] */ @Singleton @Provides fun provideCleanAPKDetailApi( okHttpClient: OkHttpClient, @Named("gsonCustomAdapter") gson: Gson - ): CleanApkAppDetailApi { + ): CleanApkAppDetailsRetrofit { return Retrofit.Builder() - .baseUrl(CleanAPKInterface.BASE_URL) + .baseUrl(CleanApkRetrofit.BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() - .create(CleanApkAppDetailApi::class.java) + .create(CleanApkAppDetailsRetrofit::class.java) } @Singleton diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt b/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt index 047395ded9c6824c172778f5ce03f479626eba9e..b574a7b9efe0547cd317044eaccd4aa492f85240 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt @@ -34,4 +34,4 @@ class BuildInfo() { this.versionCode = versionCode ?: "" this.versionName = versionName ?: "" } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt index 67948798b39cc2a32e1941d98d47a352c88f7110..6cc7b55a8e4120acb25d033ecfa128a4a9c40add 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt @@ -21,8 +21,8 @@ package foundation.e.apps.api.fused import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData 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 @@ -31,22 +31,25 @@ 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.cleanapk.CleanAPKInterface -import foundation.e.apps.api.cleanapk.CleanAPKRepository +import foundation.e.apps.api.cleanapk.CleanApkDownloadInfoFetcher +import foundation.e.apps.api.cleanapk.CleanApkRepository +import foundation.e.apps.api.cleanapk.CleanApkRetrofit +import foundation.e.apps.api.cleanapk.data.app.Application 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 import foundation.e.apps.api.fused.data.FusedCategory import foundation.e.apps.api.fused.data.FusedHome import foundation.e.apps.api.fused.data.Ratings +import foundation.e.apps.api.fused.utils.CategoryType import foundation.e.apps.api.fused.utils.CategoryUtils -import foundation.e.apps.api.gplay.GPlayAPIRepository +import foundation.e.apps.api.gplay.GplayStoreRepository import foundation.e.apps.home.model.HomeChildFusedAppDiffUtil import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.pkg.PkgManagerModule @@ -65,21 +68,27 @@ 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 javax.inject.Inject +import javax.inject.Named import javax.inject.Singleton +typealias GplaySearchResultFlow = Flow, Boolean>>> typealias FusedHomeDeferred = Deferred>> @Singleton class FusedAPIImpl @Inject constructor( - private val cleanAPKRepository: CleanAPKRepository, - private val gPlayAPIRepository: GPlayAPIRepository, private val pkgManagerModule: PkgManagerModule, private val pwaManagerModule: PWAManagerModule, private val preferenceManagerModule: PreferenceManagerModule, private val fdroidWebInterface: FdroidWebInterface, + @Named("gplayRepository") private val gplayRepository: GplayStoreRepository, + @Named("cleanApkAppsRepository") private val cleanApkAppsRepository: CleanApkRepository, + @Named("cleanApkPWARepository") private val cleanApkPWARepository: CleanApkRepository, @ApplicationContext private val context: Context ) { @@ -99,8 +108,6 @@ class FusedAPIImpl @Inject constructor( private const val CATEGORY_OPEN_GAMES_TITLE = "Open games" } - private var TAG = FusedAPIImpl::class.java.simpleName - /** * Check if list in all the FusedHome is empty. * If any list is not empty, send false. @@ -170,20 +177,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).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).body() response?.home?.let { priorList.addAll(generateCleanAPKHome(it, APP_TYPE_PWA)) } @@ -223,15 +226,14 @@ class FusedAPIImpl @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ suspend fun getCategoriesList( - type: Category.Type, - authData: AuthData + type: CategoryType, ): Triple, String, ResultStatus> { val categoriesList = mutableListOf() val preferredApplicationType = preferenceManagerModule.preferredApplicationType() var apiStatus: ResultStatus = ResultStatus.OK var applicationCategoryType = preferredApplicationType - handleAllSourcesCategories(categoriesList, type, authData).run { + handleAllSourcesCategories(categoriesList, type).run { if (first != ResultStatus.OK) { apiStatus = first applicationCategoryType = second @@ -260,7 +262,7 @@ class FusedAPIImpl @Inject constructor( */ return liveData { val packageSpecificResults = ArrayList() - fetchPackageSpecificResult(authData, query, packageSpecificResults)?.let { + fetchPackageSpecificResult(authData, query, packageSpecificResults).let { if (it.data?.second == true) { // if there are no data to load emit(it) return@liveData @@ -293,10 +295,9 @@ class FusedAPIImpl @Inject constructor( emitSource( fetchGplaySearchResults( query, - authData, searchResult, packageSpecificResults - ) + ).asLiveData() ) } } @@ -310,11 +311,9 @@ class FusedAPIImpl @Inject constructor( ): ResultSupreme, Boolean>> { val pwaApps: MutableList = mutableListOf() val status = fusedAPIImpl.runCodeBlockWithTimeout({ - getCleanAPKSearchResults( - query, - CleanAPKInterface.APP_SOURCE_ANY, - CleanAPKInterface.APP_TYPE_PWA - ).apply { + val apps = + cleanApkPWARepository.getSearchResult(query).body()?.apps + apps?.apply { if (this.isNotEmpty()) { pwaApps.addAll(this) } @@ -338,27 +337,25 @@ class FusedAPIImpl @Inject constructor( ) } - private fun fetchGplaySearchResults( + private suspend fun fetchGplaySearchResults( query: String, - authData: AuthData, searchResult: MutableList, packageSpecificResults: ArrayList - ): LiveData, Boolean>>> = - getGplaySearchResults(query, authData).map { - if (it.first.isNotEmpty()) { - searchResult.addAll(it.first) - } - ResultSupreme.Success( - Pair( - filterWithKeywordSearch( - searchResult, - packageSpecificResults, - query - ), - it.second - ) - ) + ): GplaySearchResultFlow = getGplaySearchResult(query).map { + if (it.first.isNotEmpty()) { + searchResult.addAll(it.first) } + ResultSupreme.Success( + Pair( + filterWithKeywordSearch( + searchResult, + packageSpecificResults, + query + ), + it.second + ) + ) + } private suspend fun fetchOpenSourceSearchResult( fusedAPIImpl: FusedAPIImpl, @@ -392,7 +389,7 @@ class FusedAPIImpl @Inject constructor( authData: AuthData, query: String, packageSpecificResults: MutableList - ): ResultSupreme, Boolean>>? { + ): ResultSupreme, Boolean>> { var gplayPackageResult: FusedApp? = null var cleanapkPackageResult: FusedApp? = null @@ -481,10 +478,11 @@ class FusedAPIImpl @Inject constructor( private suspend fun getCleanapkSearchResult(packageName: String): ResultSupreme { var fusedApp = FusedApp() val status = runCodeBlockWithTimeout({ - val result = cleanAPKRepository.searchApps( - keyword = packageName, - by = "package_name" + val result = cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" ).body() + if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = result.apps[0] } @@ -492,23 +490,21 @@ class FusedAPIImpl @Inject constructor( return ResultSupreme.create(status, fusedApp) } - suspend fun getSearchSuggestions(query: String, authData: AuthData): List { - return gPlayAPIRepository.getSearchSuggestions(query, authData) + suspend fun getSearchSuggestions(query: String): List { + return gplayRepository.getSearchSuggestions(query) } suspend fun getOnDemandModule( - authData: AuthData, packageName: String, moduleName: String, versionCode: Int, offerType: Int ): String? { - val list = gPlayAPIRepository.getOnDemandModule( + val list = gplayRepository.getOnDemandModule( packageName, moduleName, versionCode, offerType, - authData ) for (element in list) { if (element.name == "$moduleName.apk") { @@ -519,24 +515,27 @@ class FusedAPIImpl @Inject constructor( } suspend fun updateFusedDownloadWithDownloadingInfo( - authData: AuthData, origin: Origin, fusedDownload: FusedDownload ) { val list = mutableListOf() when (origin) { Origin.CLEANAPK -> { - val downloadInfo = cleanAPKRepository.getDownloadInfo(fusedDownload.id).body() + val downloadInfo = + (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo( + fusedDownload.id + ) + .body() downloadInfo?.download_data?.download_link?.let { list.add(it) } fusedDownload.signature = downloadInfo?.download_data?.signature ?: "" } Origin.GPLAY -> { - val downloadList = gPlayAPIRepository.getDownloadInfo( - fusedDownload.packageName, - fusedDownload.versionCode, - fusedDownload.offerType, - authData - ) + val downloadList = + gplayRepository.getDownloadInfo( + fusedDownload.packageName, + fusedDownload.versionCode, + fusedDownload.offerType + ) fusedDownload.files = downloadList list.addAll(downloadList.map { it.url }) } @@ -547,7 +546,7 @@ class FusedAPIImpl @Inject constructor( } suspend fun getOSSDownloadInfo(id: String, version: String?) = - cleanAPKRepository.getDownloadInfo(id, version) + (cleanApkAppsRepository as CleanApkDownloadInfoFetcher).getDownloadInfo(id, version) suspend fun getPWAApps(category: String): ResultSupreme> { val list = mutableListOf() @@ -578,50 +577,47 @@ class FusedAPIImpl @Inject constructor( } suspend fun getNextStreamBundle( - authData: AuthData, homeUrl: String, currentStreamBundle: StreamBundle, ): ResultSupreme { var streamBundle = StreamBundle() val status = runCodeBlockWithTimeout({ streamBundle = - gPlayAPIRepository.getNextStreamBundle(authData, homeUrl, currentStreamBundle) + gplayRepository.getAppsByCategory(homeUrl, currentStreamBundle) as StreamBundle }) return ResultSupreme.create(status, streamBundle) } suspend fun getAdjustedFirstCluster( - authData: AuthData, streamBundle: StreamBundle, pointer: Int = 0, ): ResultSupreme { var streamCluster = StreamCluster() val status = runCodeBlockWithTimeout({ streamCluster = - gPlayAPIRepository.getAdjustedFirstCluster(authData, streamBundle, pointer) + gplayRepository.getAppsByCategory("", Pair(streamBundle, pointer)) as StreamCluster }) return ResultSupreme.create(status, streamCluster) } suspend fun getNextStreamCluster( - authData: AuthData, currentStreamCluster: StreamCluster, ): ResultSupreme { var streamCluster = StreamCluster() val status = runCodeBlockWithTimeout({ - streamCluster = gPlayAPIRepository.getNextStreamCluster(authData, currentStreamCluster) + streamCluster = + gplayRepository.getAppsByCategory("", currentStreamCluster) as StreamCluster }) return ResultSupreme.create(status, streamCluster) } suspend fun getPlayStoreApps( browseUrl: String, - authData: AuthData ): ResultSupreme> { val list = mutableListOf() val status = runCodeBlockWithTimeout({ list.addAll( - gPlayAPIRepository.listApps(browseUrl, authData).map { app -> + (gplayRepository.getAppsByCategory(browseUrl) as List).map { app -> app.transformToFusedApp() } ) @@ -638,13 +634,14 @@ class FusedAPIImpl @Inject constructor( suspend fun getCleanapkAppDetails(packageName: String): Pair { var fusedApp = FusedApp() val status = runCodeBlockWithTimeout({ - val result = cleanAPKRepository.searchApps( - keyword = packageName, - by = "package_name" + val result = cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" ).body() + if (result?.apps?.isNotEmpty() == true && result.numberOfResults == 1) { fusedApp = - cleanAPKRepository.getAppOrPWADetailsByID(result.apps[0]._id).body()?.app + (cleanApkAppsRepository.getAppDetails(result.apps[0]._id) as Response).body()?.app ?: FusedApp() } fusedApp.updateFilterLevel(null) @@ -694,9 +691,9 @@ class FusedAPIImpl @Inject constructor( */ for (packageName in packageNameList) { status = runCodeBlockWithTimeout({ - cleanAPKRepository.searchApps( - keyword = packageName, - by = "package_name" + cleanApkAppsRepository.getSearchResult( + packageName, + "package_name" ).body()?.run { if (apps.isNotEmpty() && numberOfResults == 1) { fusedAppList.add( @@ -733,7 +730,7 @@ class FusedAPIImpl @Inject constructor( * Old code moved from getApplicationDetails() */ val status = runCodeBlockWithTimeout({ - gPlayAPIRepository.getAppDetails(packageNameList, authData).forEach { app -> + 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. @@ -815,7 +812,7 @@ class FusedAPIImpl @Inject constructor( * Check if app details can be shown. If not then remove the app from lists. */ try { - gPlayAPIRepository.getAppDetails(fusedApp.package_name, authData) + gplayRepository.getAppDetails(fusedApp.package_name) } catch (e: Exception) { return FilterLevel.DATA } @@ -825,11 +822,10 @@ class FusedAPIImpl @Inject constructor( * If not then change "Install" button to "N/A" */ try { - gPlayAPIRepository.getDownloadInfo( + gplayRepository.getDownloadInfo( fusedApp.package_name, fusedApp.latest_version_code, fusedApp.offer_type, - authData ) } catch (e: Exception) { return FilterLevel.UI @@ -865,9 +861,9 @@ class FusedAPIImpl @Inject constructor( val status = runCodeBlockWithTimeout({ response = if (origin == Origin.CLEANAPK) { - cleanAPKRepository.getAppOrPWADetailsByID(id).body()?.app + (cleanApkAppsRepository.getAppDetails(id) as Response).body()?.app } else { - val app = gPlayAPIRepository.getAppDetails(packageName, authData) + val app = gplayRepository.getAppDetails(packageName) as App? app?.transformToFusedApp() } response?.let { @@ -881,45 +877,6 @@ class FusedAPIImpl @Inject constructor( return Pair(response ?: FusedApp(), status) } - /* - * Categories-related internal functions - */ - - private suspend fun handleCleanApkCategories( - preferredApplicationType: String, - categoriesList: MutableList, - type: Category.Type - ): ResultStatus { - return runCodeBlockWithTimeout({ - val data = getCleanApkCategories(preferredApplicationType) - data?.let { category -> - categoriesList.addAll( - getFusedCategoryBasedOnCategoryType( - category, - type, - getCategoryTag(preferredApplicationType) - ) - ) - } - }) - } - - private fun getCategoryTag(preferredApplicationType: String): AppTag { - return if (preferredApplicationType == APP_TYPE_OPEN) { - AppTag.OpenSource(context.getString(R.string.open_source)) - } else { - AppTag.PWA(context.getString(R.string.pwa)) - } - } - - private suspend fun getCleanApkCategories(preferredApplicationType: String): Categories? { - return if (preferredApplicationType == APP_TYPE_OPEN) { - getOpenSourceCategories() - } else { - getPWAsCategories() - } - } - /* * Function to populate a given category list, from all GPlay categories, open source categories, * and PWAs. @@ -932,8 +889,7 @@ class FusedAPIImpl @Inject constructor( */ private suspend fun handleAllSourcesCategories( categoriesList: MutableList, - type: Category.Type, - authData: AuthData + type: CategoryType, ): Pair { var apiStatus = ResultStatus.OK var errorApplicationCategory = "" @@ -955,7 +911,6 @@ class FusedAPIImpl @Inject constructor( if (preferenceManagerModule.isGplaySelected()) { val gplayCategoryResult = fetchGplayCategories( type, - authData ) categoriesList.addAll(gplayCategoryResult.second) apiStatus = gplayCategoryResult.first @@ -965,15 +920,14 @@ class FusedAPIImpl @Inject constructor( return Pair(apiStatus, errorApplicationCategory) } - private suspend fun FusedAPIImpl.fetchGplayCategories( - type: Category.Type, - authData: AuthData, + private suspend fun fetchGplayCategories( + type: CategoryType, ): Triple, String> { var errorApplicationCategory = "" var apiStatus = ResultStatus.OK val categoryList = mutableListOf() runCodeBlockWithTimeout({ - val playResponse = gPlayAPIRepository.getCategoriesList(type, authData).map { app -> + val playResponse = gplayRepository.getCategories(type).map { app -> val category = app.transformToFusedCategory() updateCategoryDrawable(category) category @@ -989,8 +943,8 @@ class FusedAPIImpl @Inject constructor( return Triple(apiStatus, categoryList, errorApplicationCategory) } - private suspend fun FusedAPIImpl.fetchPWACategories( - type: Category.Type, + private suspend fun fetchPWACategories( + type: CategoryType, ): Triple, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK @@ -1013,8 +967,8 @@ class FusedAPIImpl @Inject constructor( return Triple(apiStatus, fusedCategoriesList, errorApplicationCategory) } - private suspend fun FusedAPIImpl.fetchOpenSourceCategories( - type: Category.Type, + private suspend fun fetchOpenSourceCategories( + type: CategoryType, ): Triple, String> { var errorApplicationCategory = "" var apiStatus: ResultStatus = ResultStatus.OK @@ -1091,14 +1045,14 @@ class FusedAPIImpl @Inject constructor( private fun getFusedCategoryBasedOnCategoryType( categories: Categories, - categoryType: Category.Type, + categoryType: CategoryType, tag: AppTag ): List { return when (categoryType) { - Category.Type.APPLICATION -> { + CategoryType.APPLICATION -> { getAppsCategoriesAsFusedCategory(categories, tag) } - Category.Type.GAME -> { + CategoryType.GAMES -> { getGamesCategoriesAsFusedCategory(categories, tag) } } @@ -1148,32 +1102,22 @@ class FusedAPIImpl @Inject constructor( } private suspend fun getPWAsCategories(): Categories? { - return cleanAPKRepository.getCategoriesList( - CleanAPKInterface.APP_TYPE_PWA, - CleanAPKInterface.APP_SOURCE_ANY - ).body() + return cleanApkPWARepository.getCategories().body() } private suspend fun getOpenSourceCategories(): Categories? { - return cleanAPKRepository.getCategoriesList( - CleanAPKInterface.APP_TYPE_ANY, - CleanAPKInterface.APP_SOURCE_FOSS - ).body() + return cleanApkAppsRepository.getCategories().body() } private suspend fun getOpenSourceAppsResponse(category: String): Search? { - return cleanAPKRepository.listApps( + return cleanApkAppsRepository.getAppsByCategory( category, - CleanAPKInterface.APP_SOURCE_FOSS, - CleanAPKInterface.APP_TYPE_ANY ).body() } private suspend fun getPWAAppsResponse(category: String): Search? { - return cleanAPKRepository.listApps( + return cleanApkPWARepository.getAppsByCategory( category, - CleanAPKInterface.APP_SOURCE_ANY, - CleanAPKInterface.APP_TYPE_PWA ).body() } @@ -1193,35 +1137,30 @@ class FusedAPIImpl @Inject constructor( private suspend fun getCleanAPKSearchResults( keyword: String, - source: String = CleanAPKInterface.APP_SOURCE_FOSS, - type: String = CleanAPKInterface.APP_TYPE_ANY, - nres: Int = 20, - page: Int = 1, - by: String? = null + source: String = CleanApkRetrofit.APP_SOURCE_FOSS, ): List { val list = mutableListOf() val response = - cleanAPKRepository.searchApps(keyword, source, type, nres, page, by).body()?.apps + cleanApkAppsRepository.getSearchResult(keyword).body()?.apps response?.forEach { it.updateStatus() it.updateType() it.source = - if (source.contentEquals(CleanAPKInterface.APP_SOURCE_FOSS)) "Open Source" else "PWA" + if (source.contentEquals(CleanApkRetrofit.APP_SOURCE_FOSS)) "Open Source" else "PWA" list.add(it) } return list } - private fun getGplaySearchResults( + private suspend fun getGplaySearchResult( query: String, - authData: AuthData - ): LiveData, Boolean>> { - val searchResults = - gPlayAPIRepository.getSearchResults(query, authData, ::replaceWithFDroid) + ): Flow, Boolean>> { + val searchResults = gplayRepository.getSearchResult(query) return searchResults.map { + val fusedAppList = it.first.map { app -> replaceWithFDroid(app) } Pair( - it.first, + fusedAppList, it.second ) } @@ -1348,24 +1287,16 @@ class FusedAPIImpl @Inject constructor( private suspend fun fetchGPlayHome(authData: AuthData): List { val list = mutableListOf() - 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> + 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 } diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index 7cd9bdcf13832066a93c2ef8145326019a47ac4f..4af6c8c3c2e09e2531cb81191e0d7c004a8d03b8 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -22,13 +22,13 @@ 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.Category import com.aurora.gplayapi.data.models.StreamBundle import com.aurora.gplayapi.data.models.StreamCluster import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.fused.data.FusedCategory import foundation.e.apps.api.fused.data.FusedHome +import foundation.e.apps.api.fused.utils.CategoryType import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.utils.enums.FilterLevel import foundation.e.apps.utils.enums.Origin @@ -128,7 +128,6 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII fusedDownload: FusedDownload ) { fusedAPIImpl.updateFusedDownloadWithDownloadingInfo( - authData, origin, fusedDownload ) @@ -138,24 +137,22 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII fusedAPIImpl.getOSSDownloadInfo(id, version) suspend fun getOnDemandModule( - authData: AuthData, packageName: String, moduleName: String, versionCode: Int, offerType: Int ): String? { - return fusedAPIImpl.getOnDemandModule(authData, packageName, moduleName, versionCode, offerType) + return fusedAPIImpl.getOnDemandModule(packageName, moduleName, versionCode, offerType) } suspend fun getCategoriesList( - type: Category.Type, - authData: AuthData + type: CategoryType, ): Triple, String, ResultStatus> { - return fusedAPIImpl.getCategoriesList(type, authData) + return fusedAPIImpl.getCategoriesList(type) } suspend fun getSearchSuggestions(query: String, authData: AuthData): List { - return fusedAPIImpl.getSearchSuggestions(query, authData) + return fusedAPIImpl.getSearchSuggestions(query) } fun getSearchResults( @@ -166,11 +163,10 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII } suspend fun getNextStreamBundle( - authData: AuthData, homeUrl: String, currentStreamBundle: StreamBundle, ): ResultSupreme { - return fusedAPIImpl.getNextStreamBundle(authData, homeUrl, currentStreamBundle).apply { + return fusedAPIImpl.getNextStreamBundle(homeUrl, currentStreamBundle).apply { if (isValidData()) streamBundle = data!! hasNextStreamBundle = streamBundle.hasNext() clusterPointer = 0 @@ -178,30 +174,27 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII } suspend fun getAdjustedFirstCluster( - authData: AuthData, streamBundle: StreamBundle, pointer: Int = 0, ): ResultSupreme { - return fusedAPIImpl.getAdjustedFirstCluster(authData, streamBundle, pointer) + return fusedAPIImpl.getAdjustedFirstCluster(streamBundle, pointer) } suspend fun getNextStreamCluster( - authData: AuthData, currentStreamCluster: StreamCluster, ): ResultSupreme { - return fusedAPIImpl.getNextStreamCluster(authData, currentStreamCluster) + return fusedAPIImpl.getNextStreamCluster(currentStreamCluster) } suspend fun getAppsListBasedOnCategory( category: String, browseUrl: String, - authData: AuthData, source: String ): ResultSupreme> { return when (source) { "Open Source" -> fusedAPIImpl.getOpenSourceApps(category) "PWA" -> fusedAPIImpl.getPWAApps(category) - else -> fusedAPIImpl.getPlayStoreApps(browseUrl, authData) + else -> fusedAPIImpl.getPlayStoreApps(browseUrl) } } @@ -232,7 +225,6 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII getAppsListBasedOnCategory( category, browseUrl, - authData, source ) } else { @@ -351,7 +343,7 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII } } } else if (hasNextStreamBundle) { - getNextStreamBundle(authData, browseUrl).run { + getNextStreamBundle(browseUrl).run { if (!isSuccess()) { return ResultSupreme.replicate(this, listOf()) } @@ -402,10 +394,9 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII * @see getNextDataSet */ private suspend fun getNextStreamBundle( - authData: AuthData, browseUrl: String, ): ResultSupreme { - return getNextStreamBundle(authData, browseUrl, streamBundle).apply { + return getNextStreamBundle(browseUrl, streamBundle).apply { if (isValidData()) streamBundle = data!! hasNextStreamBundle = streamBundle.hasNext() clusterPointer = 0 @@ -423,7 +414,7 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII private suspend fun getAdjustedFirstCluster( authData: AuthData, ): ResultSupreme { - return getAdjustedFirstCluster(authData, streamBundle, clusterPointer) + return getAdjustedFirstCluster(streamBundle, clusterPointer) .apply { if (isValidData()) addNewClusterData(this.data!!) } @@ -440,7 +431,7 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII private suspend fun getNextStreamCluster( authData: AuthData, ): ResultSupreme { - return getNextStreamCluster(authData, streamCluster).apply { + return getNextStreamCluster(streamCluster).apply { if (isValidData()) addNewClusterData(this.data!!) } } diff --git a/app/src/main/java/foundation/e/apps/api/fused/utils/CategoryUtils.kt b/app/src/main/java/foundation/e/apps/api/fused/utils/CategoryUtils.kt index c6048a94a243307295f5a5e35f140584cba2f982..8b203a6b71eae0fe47e528cdb4a75a0213545e5b 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/utils/CategoryUtils.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/utils/CategoryUtils.kt @@ -176,3 +176,7 @@ object CategoryUtils { } } } + +enum class CategoryType { + APPLICATION, GAMES +} diff --git a/app/src/main/java/foundation/e/apps/api/gplay/GplayStoreRepository.kt b/app/src/main/java/foundation/e/apps/api/gplay/GplayStoreRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..65651eac00af4b5a6bbb2abb6d6fc6c680029b50 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/gplay/GplayStoreRepository.kt @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +package foundation.e.apps.api.gplay + +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 foundation.e.apps.api.BaseStoreRepository +import foundation.e.apps.api.fused.utils.CategoryType +import kotlinx.coroutines.flow.Flow + +interface GplayStoreRepository : BaseStoreRepository { + suspend fun getSearchResult(query: String): Flow, Boolean>> + suspend fun getSearchSuggestions(query: String): List + suspend fun getAppsByCategory(category: String, paginationParameter: Any? = null): Any + suspend fun getCategories(type: CategoryType? = null): List + suspend fun getAppsDetails(packageNamesOrIds: List): List + suspend fun getDownloadInfo( + idOrPackageName: String, + versionCode: Any? = null, + offerType: Int = -1 + ): List + suspend fun getOnDemandModule( + packageName: String, + moduleName: String, + versionCode: Int, + offerType: Int + ): List +} diff --git a/app/src/main/java/foundation/e/apps/api/gplay/GplayStoreRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/api/gplay/GplayStoreRepositoryImpl.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd9fcffce9f7d12a42bfc71ec9273beb295870ca --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/gplay/GplayStoreRepositoryImpl.kt @@ -0,0 +1,507 @@ +/* + * 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 . + */ + +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.Category +import com.aurora.gplayapi.data.models.File +import com.aurora.gplayapi.data.models.SearchBundle +import com.aurora.gplayapi.data.models.StreamBundle +import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.gplayapi.helpers.AppDetailsHelper +import com.aurora.gplayapi.helpers.CategoryHelper +import com.aurora.gplayapi.helpers.ExpandedBrowseHelper +import com.aurora.gplayapi.helpers.PurchaseHelper +import com.aurora.gplayapi.helpers.SearchHelper +import com.aurora.gplayapi.helpers.StreamHelper +import com.aurora.gplayapi.helpers.TopChartsHelper +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.api.fused.utils.CategoryType +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.supervisorScope +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class GplayStoreRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val gPlayHttpClient: GPlayHttpClient, + private val loginSourceRepository: LoginSourceRepository +) : GplayStoreRepository { + + private val authData by lazy { loginSourceRepository.gplayAuth!! } + + override suspend fun getHomeScreenData(): Any { + val homeScreenData = mutableMapOf>() + val homeElements = createTopChartElements() + + homeElements.forEach { + val chart = it.value.keys.iterator().next() + val type = it.value.values.iterator().next() + val result = getTopApps(type, chart, authData) + 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, 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(authData).using(gPlayHttpClient) + val searchBundle = searchHelper.searchResults(query) + + val initialReplacedList = mutableListOf() + val INITIAL_LIMIT = 4 + + emitReplacedList( + this@flow, + initialReplacedList, + INITIAL_LIMIT, + searchBundle, + true, + ) + + var nextSubBundleSet: MutableSet + 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, Boolean>>, + accumulationList: MutableList, + accumulationLimit: Int, + ): MutableSet { + 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 { + val searchData = mutableListOf() + withContext(Dispatchers.IO) { + val searchHelper = SearchHelper(authData).using(gPlayHttpClient) + searchData.addAll(searchHelper.searchSuggestions(query)) + } + return searchData.filter { it.suggestedQuery.isNotBlank() } + } + + override suspend fun getAppsByCategory(category: String, paginationParameter: Any?): Any { + if (paginationParameter != null && paginationParameter is StreamCluster) { + return getNextStreamCluster(authData, paginationParameter) + } + + if (paginationParameter != null && paginationParameter is StreamBundle) { + return getNextStreamBundle( + authData, + category, + paginationParameter + ) + } + + if (paginationParameter != null && paginationParameter is Pair<*, *>) { + return getAdjustedFirstCluster( + authData, + paginationParameter.first as StreamBundle, + paginationParameter.second as Int + ) + } + + return getGplayApps(category) + } + + override suspend fun getCategories(type: CategoryType?): List { + val categoryList = mutableListOf() + if (type == null) { + return categoryList + } + + withContext(Dispatchers.IO) { + val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) + categoryList.addAll(categoryHelper.getAllCategoriesList(getCategoryType(type))) + } + return categoryList + } + + override suspend fun getAppDetails(packageNameOrId: String): App? { + var appDetails: App? + withContext(Dispatchers.IO) { + val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) + appDetails = appDetailsHelper.getAppByPackageName(packageNameOrId) + } + return appDetails + } + + override suspend fun getAppsDetails(packageNamesOrIds: List): List { + val appDetailsList = mutableListOf() + withContext(Dispatchers.IO) { + val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) + appDetailsList.addAll(appDetailsHelper.getAppByPackageName(packageNamesOrIds)) + } + return appDetailsList + } + + private fun getCategoryType(type: CategoryType): Category.Type { + return if (type == CategoryType.APPLICATION) Category.Type.APPLICATION else Category.Type.GAME + } + + private suspend fun getGplayApps(category: String): List { + val list = mutableListOf() + withContext(Dispatchers.IO) { + supervisorScope { + val categoryHelper = + CategoryHelper(authData).using(gPlayHttpClient) + + var streamBundle: StreamBundle + var nextStreamBundleUrl = category + + /* + * Logic: We start with the browseUrl. + * When we call getSubCategoryBundle(), we get a new StreamBundle object, having + * StreamClusters, which have app data. + * The generated StreamBundle also has a url for next StreamBundle to be generated + * with fresh app data. + * Hence we loop as long as the StreamBundle's next page url is not blank. + */ + do { + streamBundle = categoryHelper.getSubCategoryBundle(nextStreamBundleUrl) + val streamClusters = streamBundle.streamClusters + + /* + * Similarly to the logic of StreamBundles, each StreamCluster can have a url, + * pointing to another StreamCluster with new set of app data. + * We loop over all the StreamCluster of one StreamBundle, and for each of the + * StreamCluster we continue looping as long as the StreamCluster.clusterNextPageUrl + * is not blank. + */ + streamClusters.values.forEach { streamCluster -> + list.addAll(streamCluster.clusterAppList) // Add all apps for this StreamCluster + + // Loop over possible next StreamClusters + var currentStreamCluster = streamCluster + while (currentStreamCluster.hasNext()) { + currentStreamCluster = categoryHelper + .getNextStreamCluster(currentStreamCluster.clusterNextPageUrl) + .also { + list.addAll(it.clusterAppList) + } + } + } + + nextStreamBundleUrl = streamBundle.streamNextPageUrl + } while (streamBundle.hasNext()) + } + } + return list.distinctBy { it.packageName } + } + + /* + * Get next StreamCluster from currentNextPageUrl. + * This method is to be called when the scrollview reaches the bottom. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + private suspend fun getNextStreamCluster( + authData: AuthData, + currentStreamCluster: StreamCluster, + ): StreamCluster { + return withContext(Dispatchers.IO) { + if (!currentStreamCluster.hasNext()) { + return@withContext StreamCluster() + } + + /* + * Logic found in Aurora store code. + */ + return@withContext if (currentStreamCluster.clusterNextPageUrl.contains("expanded")) { + getExpandedStreamCluster( + currentStreamCluster.clusterNextPageUrl, + authData + ) + } else { + getStreamCluster(currentStreamCluster.clusterNextPageUrl, authData) + } + } + } + + private fun getExpandedStreamCluster( + url: String, + authData: AuthData, + checkStartUrl: Boolean = false + ): StreamCluster { + ExpandedBrowseHelper(authData).using(gPlayHttpClient).run { + + if (!checkStartUrl) { + return getExpandedBrowseClusters(url) + } + + val browseResponse = getBrowseStreamResponse(url) + + if (browseResponse.hasBrowseTab()) { + return getExpandedBrowseClusters(browseResponse.browseTab.listUrl) + } + } + return StreamCluster() + } + + private fun getStreamCluster( + url: String, + authData: AuthData, + checkStartUrl: Boolean = false + ): StreamCluster { + StreamHelper(authData).using(gPlayHttpClient).run { + + if (!checkStartUrl) { + return getNextStreamCluster(url) + } + + val browseResponse = getBrowseStreamResponse(url) + + if (browseResponse.contentsUrl.isNotEmpty()) { + return getNextStreamCluster(browseResponse.contentsUrl) + } + + if (browseResponse.hasBrowseTab()) { + return getNextStreamCluster(browseResponse.browseTab.listUrl) + } + } + return StreamCluster() + } + + suspend fun getNextStreamBundle( + authData: AuthData, + homeUrl: String, + currentStreamBundle: StreamBundle, + ): StreamBundle { + return withContext(Dispatchers.IO) { + val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) + if (currentStreamBundle.streamClusters.isEmpty()) { + categoryHelper.getSubCategoryBundle(homeUrl) + } else { + categoryHelper.getSubCategoryBundle(currentStreamBundle.streamNextPageUrl) + } + } + } + + /** + * Get first adjusted StreamCluster of a StreamBundle. + * + * Takes the clusterBrowseUrl of streamBundle.streamClusters[[pointer]], + * Populates the cluster and returns it. + * + * This does not always operate on zeroth StreamCluster of [streamBundle]. + * A StreamBundle can have many StreamClusters, each of the individual StreamCluster can point + * to completely different StreamClusters. + * + * StreamBundle 1 (streamNextPageUrl points to StreamBundle 2) + * StreamCluster 1 -> StreamCluster 1.1 -> StreamCluster 1.2 .... + * StreamCluster 2 -> StreamCluster 2.1 -> StreamCluster 2.2 .... + * StreamCluster 3 -> StreamCluster 3.1 -> StreamCluster 3.2 .... + * + * Here [pointer] refers to the position of StreamCluster 1, 2, 3.... but not 1.1, 2.1 .... + * The subsequent clusters (1.1, 1.2, .. 2.1 ..) are accessed by [getNextStreamCluster]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + suspend fun getAdjustedFirstCluster( + authData: AuthData, + streamBundle: StreamBundle, + pointer: Int = 0, + ): StreamCluster { + return withContext(Dispatchers.IO) { + val clusterSize = streamBundle.streamClusters.size + if (clusterSize != 0 && pointer < clusterSize && pointer >= 0) { + val firstCluster = streamBundle.streamClusters.values.toList()[pointer] + + val clusterBrowseUrl = firstCluster.clusterBrowseUrl + + /* + * Logic found in Aurora store code. + */ + val adjustedCluster = if (firstCluster.clusterBrowseUrl.contains("expanded")) { + getExpandedStreamCluster(clusterBrowseUrl, authData, true) + } else { + getStreamCluster(clusterBrowseUrl, authData, true) + } + + return@withContext adjustedCluster.apply { + clusterAppList.addAll(firstCluster.clusterAppList) + if (!hasNext()) { + clusterNextPageUrl = firstCluster.clusterNextPageUrl + } + } + } + + /* + * If nothing works return blank StreamCluster. + */ + StreamCluster() + } + } + + private suspend fun emitReplacedList( + scope: FlowCollector, Boolean>>, + accumulationList: MutableList, + 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, Boolean>>, + it: List, + moreToEmit: Boolean + ) { + scope.emit(Pair(it, moreToEmit)) + } + + private suspend fun getTopApps( + type: TopChartsHelper.Type, + chart: TopChartsHelper.Chart, + authData: AuthData + ): List { + val topApps = mutableListOf() + withContext(Dispatchers.IO) { + val topChartsHelper = TopChartsHelper(authData).using(gPlayHttpClient) + topApps.addAll(topChartsHelper.getCluster(type, chart).clusterAppList) + } + return topApps + } + + override suspend fun getDownloadInfo( + idOrPackageName: String, + versionCode: Any?, + offerType: Int + ): List { + val downloadData = mutableListOf() + withContext(Dispatchers.IO) { + val version = versionCode?.let { it as Int } ?: -1 + val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) + downloadData.addAll(purchaseHelper.purchase(idOrPackageName, version, offerType)) + } + return downloadData + } + + override suspend fun getOnDemandModule( + packageName: String, + moduleName: String, + versionCode: Int, + offerType: Int + ): List { + val downloadData = mutableListOf() + withContext(Dispatchers.IO) { + val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) + downloadData.addAll( + purchaseHelper.getOnDemandModule( + packageName, + moduleName, + versionCode, + offerType + ) + ) + } + return downloadData + } +} diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index dfbe19b105b6aa9dd19816ed647607e6874ce710..982785769c189c37b6ef1ba3baafd274b40de85b 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -49,7 +49,7 @@ import foundation.e.apps.MainActivity import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R -import foundation.e.apps.api.cleanapk.CleanAPKInterface +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.model.ApplicationScreenshotsRVAdapter import foundation.e.apps.application.subFrags.ApplicationDialogFragment @@ -364,7 +364,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { sourceTag.text = it.source } if (origin == Origin.CLEANAPK) { - appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) + appIcon.load(CleanApkRetrofit.ASSET_URL + it.icon_image_path) } else { appIcon.load(it.icon_image_path) } diff --git a/app/src/main/java/foundation/e/apps/application/model/ApplicationScreenshotsRVAdapter.kt b/app/src/main/java/foundation/e/apps/application/model/ApplicationScreenshotsRVAdapter.kt index 17ab706fd419f7d2b5d50224129526d308cb5f72..eb121340fc180dbc984667708457872a794a37b7 100644 --- a/app/src/main/java/foundation/e/apps/application/model/ApplicationScreenshotsRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/application/model/ApplicationScreenshotsRVAdapter.kt @@ -24,7 +24,7 @@ import androidx.navigation.findNavController import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import coil.load -import foundation.e.apps.api.cleanapk.CleanAPKInterface +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.application.ApplicationFragmentDirections import foundation.e.apps.databinding.ApplicationScreenshotsListItemBinding import foundation.e.apps.utils.enums.Origin @@ -53,7 +53,7 @@ class ApplicationScreenshotsRVAdapter( val imageView = holder.binding.imageView when (origin) { Origin.CLEANAPK -> { - imageView.load(CleanAPKInterface.ASSET_URL + oldList[position]) + imageView.load(CleanApkRetrofit.ASSET_URL + oldList[position]) } Origin.GPLAY -> { imageView.load(oldList[position]) diff --git a/app/src/main/java/foundation/e/apps/application/model/ScreenshotRVAdapter.kt b/app/src/main/java/foundation/e/apps/application/model/ScreenshotRVAdapter.kt index 8e76afc43d0ff6208c19dee4286fc11c1b7d6d77..61f2cb0c998e7533478ae11ed3bb8369ce876aac 100644 --- a/app/src/main/java/foundation/e/apps/application/model/ScreenshotRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/application/model/ScreenshotRVAdapter.kt @@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.CircularProgressDrawable import coil.load import foundation.e.apps.R -import foundation.e.apps.api.cleanapk.CleanAPKInterface +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.databinding.ScreenshotListItemBinding import foundation.e.apps.utils.enums.Origin @@ -57,7 +57,7 @@ class ScreenshotRVAdapter(private val list: List, private val origin: Or val imageView = holder.binding.imageView when (origin) { Origin.CLEANAPK -> { - imageView.load(CleanAPKInterface.ASSET_URL + list[position]) { + imageView.load(CleanApkRetrofit.ASSET_URL + list[position]) { placeholder(circularProgressDrawable) } } diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListRVAdapter.kt index 216b0335cd6ef8a31482a44f8da21d0508928830..3e669f31e68bbd4655efd09b8c18a3625effa933 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListRVAdapter.kt @@ -40,7 +40,7 @@ import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R -import foundation.e.apps.api.cleanapk.CleanAPKInterface +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.databinding.ApplicationListItemBinding @@ -169,7 +169,7 @@ class ApplicationListRVAdapter( } } Origin.CLEANAPK -> { - appIcon.load(CleanAPKInterface.ASSET_URL + searchApp.icon_image_path) { + appIcon.load(CleanApkRetrofit.ASSET_URL + searchApp.icon_image_path) { placeholder(shimmerDrawable) } } diff --git a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt index bf34cab5c5c2d1793301b72a11bc69fdd9d3ddcf..6838af7746eb69884971040964e91e73e7aefbdd 100644 --- a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt @@ -24,10 +24,10 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager -import com.aurora.gplayapi.data.models.Category import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R +import foundation.e.apps.api.fused.utils.CategoryType import foundation.e.apps.categories.model.CategoriesRVAdapter import foundation.e.apps.databinding.FragmentAppsBinding import foundation.e.apps.login.AuthObject @@ -73,7 +73,7 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { } override fun loadData(authObjectList: List) { - categoriesViewModel.loadData(Category.Type.APPLICATION, authObjectList) { + categoriesViewModel.loadData(CategoryType.APPLICATION, authObjectList) { clearAndRestartGPlayLogin() true } diff --git a/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt b/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt index d505cf1367da6f13cf485360680564db58f9f9c1..9cb65211255dc452eec531c22ffbf0f5f0178c55 100644 --- a/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt @@ -21,10 +21,10 @@ package foundation.e.apps.categories import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.Category import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedCategory +import foundation.e.apps.api.fused.utils.CategoryType import foundation.e.apps.login.AuthObject import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.exceptions.CleanApkException @@ -42,7 +42,7 @@ class CategoriesViewModel @Inject constructor( MutableLiveData() fun loadData( - type: Category.Type, + type: CategoryType, authObjectList: List, retryBlock: (failedObjects: List) -> Boolean, ) { @@ -60,9 +60,9 @@ class CategoriesViewModel @Inject constructor( }, retryBlock) } - fun getCategoriesList(type: Category.Type, authData: AuthData) { + fun getCategoriesList(type: CategoryType, authData: AuthData) { viewModelScope.launch { - val categoriesData = fusedAPIRepository.getCategoriesList(type, authData) + val categoriesData = fusedAPIRepository.getCategoriesList(type) categoriesList.postValue(categoriesData) val status = categoriesData.third diff --git a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt index a7c2614e2d7b82bb447917be26dd3a1a64e98633..76ccada5a1fc38aa82644031b1f22a372105052f 100644 --- a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt @@ -24,10 +24,10 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager -import com.aurora.gplayapi.data.models.Category import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R +import foundation.e.apps.api.fused.utils.CategoryType import foundation.e.apps.categories.model.CategoriesRVAdapter import foundation.e.apps.databinding.FragmentGamesBinding import foundation.e.apps.login.AuthObject @@ -73,7 +73,7 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { } override fun loadData(authObjectList: List) { - categoriesViewModel.loadData(Category.Type.GAME, authObjectList) { + categoriesViewModel.loadData(CategoryType.GAMES, authObjectList) { clearAndRestartGPlayLogin() true } diff --git a/app/src/main/java/foundation/e/apps/di/NamedRepositoryModule.kt b/app/src/main/java/foundation/e/apps/di/NamedRepositoryModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..c506d4ff5cbc499009ccb653c0a71bc0a5eba2de --- /dev/null +++ b/app/src/main/java/foundation/e/apps/di/NamedRepositoryModule.kt @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +package foundation.e.apps.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import foundation.e.apps.api.cleanapk.CleanApkAppDetailsRetrofit +import foundation.e.apps.api.cleanapk.CleanApkAppsRepositoryImpl +import foundation.e.apps.api.cleanapk.CleanApkPWARepository +import foundation.e.apps.api.cleanapk.CleanApkRepository +import foundation.e.apps.api.cleanapk.CleanApkRetrofit +import foundation.e.apps.api.gplay.GplayStoreRepository +import foundation.e.apps.api.gplay.GplayStoreRepositoryImpl +import foundation.e.apps.api.gplay.utils.GPlayHttpClient +import foundation.e.apps.login.LoginSourceRepository +import javax.inject.Named +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object NamedRepositoryModule { + @Singleton + @Provides + @Named("gplayRepository") + fun getGplayRepository( + @ApplicationContext context: Context, + gPlayHttpClient: GPlayHttpClient, + loginSourceRepository: LoginSourceRepository + ): GplayStoreRepository { + return GplayStoreRepositoryImpl(context, gPlayHttpClient, loginSourceRepository) + } + + @Singleton + @Provides + @Named("cleanApkAppsRepository") + fun getCleanApkAppsRepository( + cleanAPKRetrofit: CleanApkRetrofit, + cleanApkAppDetailsRetrofit: CleanApkAppDetailsRetrofit + ): CleanApkRepository { + return CleanApkAppsRepositoryImpl(cleanAPKRetrofit, cleanApkAppDetailsRetrofit) + } + + @Singleton + @Provides + @Named("cleanApkPWARepository") + fun getCleanApkPWARepository( + cleanAPKRetrofit: CleanApkRetrofit, + cleanApkAppDetailsRetrofit: CleanApkAppDetailsRetrofit + ): CleanApkRepository { + return CleanApkPWARepository(cleanAPKRetrofit, cleanApkAppDetailsRetrofit) + } +} diff --git a/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt index 3c59b4e44a1a17cc5836444be864168216cc231b..548af8d7d69668a8ab93bf4c39ad56d58a48926e 100644 --- a/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt @@ -35,7 +35,7 @@ import com.google.android.material.snackbar.Snackbar import foundation.e.apps.AppInfoFetchViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R -import foundation.e.apps.api.cleanapk.CleanAPKInterface +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.databinding.HomeChildListItemBinding @@ -82,7 +82,7 @@ class HomeChildRVAdapter( holder.binding.apply { if (homeApp.origin == Origin.CLEANAPK) { - appIcon.load(CleanAPKInterface.ASSET_URL + homeApp.icon_image_path) { + appIcon.load(CleanApkRetrofit.ASSET_URL + homeApp.icon_image_path) { placeholder(shimmerDrawable) } } else { diff --git a/app/src/main/java/foundation/e/apps/login/AuthDataValidator.kt b/app/src/main/java/foundation/e/apps/login/AuthDataValidator.kt index 32bcc7bd7604d7d9c8a8fcf9055323494eafccea..029558a3e8747a39f12e2bfd4bc50be657fa9aa4 100644 --- a/app/src/main/java/foundation/e/apps/login/AuthDataValidator.kt +++ b/app/src/main/java/foundation/e/apps/login/AuthDataValidator.kt @@ -23,4 +23,4 @@ import foundation.e.apps.api.ResultSupreme interface AuthDataValidator { suspend fun validateAuthData(): ResultSupreme -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt index 323ae6e823f124f64df4b8249aeb2a94974c0f46..c96106aaea4afa8d081606eb589d88afd07468c4 100644 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt +++ b/app/src/main/java/foundation/e/apps/login/LoginSourceGPlay.kt @@ -79,14 +79,14 @@ class LoginSourceGPlay @Inject constructor( val savedAuth = getSavedAuthData() val authData = ( - savedAuth ?: run { - // if no saved data, then generate new auth data. - generateAuthData().let { - if (it.isSuccess()) it.data!! - else return AuthObject.GPlayAuth(it, user) - } + savedAuth ?: run { + // if no saved data, then generate new auth data. + generateAuthData().let { + if (it.isSuccess()) it.data!! + else return AuthObject.GPlayAuth(it, user) } - ) + } + ) val formattedAuthData = formatAuthData(authData) formattedAuthData.locale = locale @@ -239,12 +239,12 @@ class LoginSourceGPlay @Inject constructor( } else { val message = "Validating AuthData failed.\n" + - "Network code: ${playResponse?.code}\n" + - "Success: ${playResponse?.isSuccessful}" + - playResponse?.errorString?.run { - if (isNotBlank()) "\nError message: $this" - else "" - } + "Network code: ${playResponse?.code}\n" + + "Success: ${playResponse?.isSuccessful}" + + playResponse?.errorString?.run { + if (isNotBlank()) "\nError message: $this" + else "" + } ResultSupreme.Error( message, diff --git a/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt b/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt index 86fc9227967ae8b2c47722ef7ec753afc51b9a4f..140de79e7d186c254903956d8c5dc0f09e09d51d 100644 --- a/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt +++ b/app/src/main/java/foundation/e/apps/login/LoginSourceRepository.kt @@ -30,6 +30,7 @@ class LoginSourceRepository @Inject constructor( private val sources: List, ) { + var gplayAuth: AuthData? = null suspend fun getAuthObjects(clearAuthTypes: List = listOf()): List { val authObjectsLocal = ArrayList() @@ -39,6 +40,9 @@ class LoginSourceRepository @Inject constructor( if (source::class.java.simpleName in clearAuthTypes) { source.clearSavedAuth() } + if (source is LoginSourceGPlay) { + gplayAuth = source.getAuthObject().result.data + } authObjectsLocal.add(source.getAuthObject()) } diff --git a/app/src/main/java/foundation/e/apps/splitinstall/SplitInstallBinder.kt b/app/src/main/java/foundation/e/apps/splitinstall/SplitInstallBinder.kt index f6783a5e584b7a1701c677020d23885bbaf27fdb..5ce322d9ebe5b378ead66aa560d9f3512c1fd7c8 100644 --- a/app/src/main/java/foundation/e/apps/splitinstall/SplitInstallBinder.kt +++ b/app/src/main/java/foundation/e/apps/splitinstall/SplitInstallBinder.kt @@ -97,15 +97,14 @@ class SplitInstallBinder( moduleName: String, versionCode: Int ): String? { - var url = fusedAPIRepository.getOnDemandModule( - authData!!, packageName, moduleName, - versionCode, 1 - ) + var url = fusedAPIRepository.getOnDemandModule(packageName, moduleName, versionCode, 1) if (url == null) { url = fusedAPIRepository.getOnDemandModule( - authData, packageName, "config.$moduleName", - versionCode, 1 + packageName, + "config.$moduleName", + versionCode, + 1 ) } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt index 7c4a002b8507e9b19a4ac533e2b3104b66f0b3f8..0ee9eab6206e31fdb069257c8c19efac866a0cc4 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt @@ -34,8 +34,8 @@ import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.PreferenceManagerModule -import javax.inject.Inject import timber.log.Timber +import javax.inject.Inject class UpdatesManagerImpl @Inject constructor( @ApplicationContext private val context: Context, @@ -90,7 +90,8 @@ class UpdatesManagerImpl @Inject constructor( // Get GPlay app updates if (getApplicationCategoryPreference().contains(APP_TYPE_ANY) && - gPlayInstalledApps.isNotEmpty()) { + gPlayInstalledApps.isNotEmpty() + ) { status = getUpdatesFromApi({ fusedAPIRepository.getApplicationDetails( @@ -239,8 +240,8 @@ class UpdatesManagerImpl @Inject constructor( Timber.i( "Signature calculated for : ${cleanApkFusedApp.package_name}, " + - "signature version: ${installedVersionSignature}, " + - "is sig blank: ${pgpSignature.isBlank()}" + "signature version: $installedVersionSignature, " + + "is sig blank: ${pgpSignature.isBlank()}" ) return downloadInfo?.signature ?: "" @@ -301,7 +302,6 @@ class UpdatesManagerImpl @Inject constructor( return "" } - // Received list has build info of the latest version at the bottom. // We want it at the top. val builds = fdroidRepository.getBuildVersionInfo(packageName)?.asReversed() ?: return "" @@ -310,7 +310,7 @@ class UpdatesManagerImpl @Inject constructor( it.versionCode == installedVersionCode && it.versionName == installedVersionName }?.run { builds.indexOf(this) - }?: return "" + } ?: return "" Timber.i("Build info match at index: $matchingIndex") diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt index 306e4b2149703041e0cb756f5d5a6608a731ed37..71ea39583b6a62fad8f2cdf68bba4924b60d88c9 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt @@ -20,7 +20,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R import foundation.e.apps.api.ResultSupreme -import foundation.e.apps.api.cleanapk.CleanAPKInterface +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.login.LoginSourceRepository @@ -318,7 +318,7 @@ class UpdatesWorker @AssistedInject constructor( private fun getIconImageToBase64(fusedApp: FusedApp): String { val url = - if (fusedApp.origin == Origin.CLEANAPK) "${CleanAPKInterface.ASSET_URL}${fusedApp.icon_image_path}" else fusedApp.icon_image_path + if (fusedApp.origin == Origin.CLEANAPK) "${CleanApkRetrofit.ASSET_URL}${fusedApp.icon_image_path}" else fusedApp.icon_image_path val stream = URL(url).openStream() val bitmap = BitmapFactory.decodeStream(stream) val byteArrayOS = ByteArrayOutputStream() diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt index 646b84fad6f0c241f16dde4fb2f5687d13aac5f4..136520bb8198ebe6531b5724fdc04cb36a74a0aa 100644 --- a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt @@ -26,8 +26,8 @@ import com.aurora.gplayapi.Constants import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category -import foundation.e.apps.api.cleanapk.CleanAPKInterface import foundation.e.apps.api.cleanapk.CleanAPKRepository +import foundation.e.apps.api.cleanapk.CleanApkRetrofit import foundation.e.apps.api.cleanapk.data.categories.Categories import foundation.e.apps.api.cleanapk.data.search.Search import foundation.e.apps.api.fdroid.FdroidWebInterface @@ -622,7 +622,7 @@ class FusedApiImplTest { Mockito.`when`( cleanApkRepository.getCategoriesList( - eq(CleanAPKInterface.APP_TYPE_PWA), eq(CleanAPKInterface.APP_SOURCE_ANY) + eq(CleanApkRetrofit.APP_TYPE_PWA), eq(CleanApkRetrofit.APP_SOURCE_ANY) ) ).thenReturn(response) @@ -646,7 +646,7 @@ class FusedApiImplTest { Mockito.`when`( cleanApkRepository.getCategoriesList( - eq(CleanAPKInterface.APP_TYPE_ANY), eq(CleanAPKInterface.APP_SOURCE_FOSS) + eq(CleanApkRetrofit.APP_TYPE_ANY), eq(CleanApkRetrofit.APP_SOURCE_FOSS) ) ).thenReturn(response) Mockito.`when`(context.getString(eq(R.string.open_source))).thenReturn("Open source") @@ -705,13 +705,13 @@ class FusedApiImplTest { Mockito.`when`( cleanApkRepository.getCategoriesList( - eq(CleanAPKInterface.APP_TYPE_ANY), eq(CleanAPKInterface.APP_SOURCE_FOSS) + eq(CleanApkRetrofit.APP_TYPE_ANY), eq(CleanApkRetrofit.APP_SOURCE_FOSS) ) ).thenReturn(openSourceResponse) Mockito.`when`( cleanApkRepository.getCategoriesList( - eq(CleanAPKInterface.APP_TYPE_PWA), eq(CleanAPKInterface.APP_SOURCE_ANY) + eq(CleanApkRetrofit.APP_TYPE_PWA), eq(CleanApkRetrofit.APP_SOURCE_ANY) ) ).thenReturn(pwaResponse) @@ -813,8 +813,8 @@ class FusedApiImplTest { Mockito.`when`( cleanApkRepository.searchApps( keyword = "com.search.package", - type = CleanAPKInterface.APP_TYPE_PWA, - source = CleanAPKInterface.APP_SOURCE_ANY + type = CleanApkRetrofit.APP_TYPE_PWA, + source = CleanApkRetrofit.APP_SOURCE_ANY ) ).thenReturn(packageNameSearchResponse)