diff --git a/app/build.gradle b/app/build.gradle index 7aeff10988798669c030076e1e15fbcf4b50192b..adfa3895ab9c1a117d09f421e0f7d86058c7bc57 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -182,7 +182,7 @@ dependencies { api files('libs/splitinstall-lib.jar') implementation 'foundation.e.lib:telemetry:0.0.11-alpha' - implementation "foundation.e:gplayapi:3.2.10-4" + implementation 'foundation.e:gplayapi:3.4.2-0' implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.6' @@ -249,9 +249,9 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" // Hilt - def hilt_version = '2.40.5' - kapt "com.google.dagger:hilt-compiler:2.44.2" - implementation "com.google.dagger:hilt-android:2.44.2" + def hilt_version = '2.51.1' + kapt "com.google.dagger:hilt-compiler:$hilt_version" + implementation "com.google.dagger:hilt-android:$hilt_version" implementation 'androidx.hilt:hilt-work:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0' @@ -271,10 +271,9 @@ dependencies { testImplementation "org.jetbrains.kotlin:kotlin-test:$test_kotlin_version" // Room - def roomVersion = "2.4.1" - kapt "androidx.room:room-compiler:$roomVersion" - implementation "androidx.room:room-ktx:$roomVersion" - implementation "androidx.room:room-runtime:$roomVersion" + kapt "androidx.room:room-compiler:2.6.1" + implementation "androidx.room:room-ktx:2.6.1" + implementation "androidx.room:room-runtime:2.6.1" // WorkManager implementation 'androidx.work:work-runtime-ktx:2.7.1' diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index c31a64f465832d216ac37ca910479fe5631bb02d..30c7e038424ea1f4e597ae29cd16a604a371aa20 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -32,7 +32,7 @@ private const val ERROR_GPLAY_API = "Gplay api has faced error!" private const val REGEX_429_OR_401 = "429|401" private const val MAX_RETRY_DELAY_IN_SECONDS = 300 private const val ONE_SECOND_IN_MILLIS = 1000L -private const val INITIAL_DELAY_RETRY_IN_SECONDS = 10 +private const val INITIAL_DELAY_RETRY_IN_SECONDS = 1 suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { return try { diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt index 8885da06f438d18de161e02479927c04e6e09576..d142e03e36d5cd6e17d26246d1772221d86a496d 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt @@ -19,7 +19,6 @@ package foundation.e.apps.data.application import com.aurora.gplayapi.Constants -import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Home import foundation.e.apps.data.enums.FilterLevel @@ -37,8 +36,8 @@ class ApplicationDataManager @Inject constructor( private val appLoungePackageManager: AppLoungePackageManager, private val pwaManager: PwaManager ) { - suspend fun updateFilterLevel(authData: AuthData?, application: Application) { - application.filterLevel = getAppFilterLevel(application, authData) + suspend fun updateFilterLevel(application: Application) { + application.filterLevel = getAppFilterLevel(application) } suspend fun prepareApps( @@ -50,20 +49,19 @@ class ApplicationDataManager @Inject constructor( appList.forEach { it.updateType() updateStatus(it) - updateFilterLevel(null, it) + updateFilterLevel(it) } list.add(Home(value, appList)) } } - suspend fun getAppFilterLevel(application: Application, authData: AuthData?): FilterLevel { + suspend fun getAppFilterLevel(application: Application): FilterLevel { return when { application.package_name.isBlank() -> FilterLevel.UNKNOWN !application.isFree && application.price.isBlank() -> FilterLevel.UI application.origin == Origin.CLEANAPK -> FilterLevel.NONE application.origin == Origin.GITLAB_RELEASES -> 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 diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt index beb4238d951e857f4963b51334a6afbc2441bdc0..9aa108c64630d5d3a8933191adee1d59ff496217 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt @@ -63,7 +63,7 @@ class AppsApiImpl @Inject constructor( appSources.cleanApkAppsRepo.getAppDetails(result.apps[0]._id) } - application.updateFilterLevel(null) + application.updateFilterLevel() } return Pair(application, result.getResultStatus()) @@ -72,8 +72,8 @@ class AppsApiImpl @Inject constructor( /* * Handy method to run on an instance of FusedApp to update its filter level. */ - private suspend fun Application.updateFilterLevel(authData: AuthData?) { - this.filterLevel = applicationDataManager.getAppFilterLevel(this, authData) + private suspend fun Application.updateFilterLevel() { + this.filterLevel = applicationDataManager.getAppFilterLevel(this) } override suspend fun getApplicationDetails( @@ -148,7 +148,7 @@ class AppsApiImpl @Inject constructor( applicationList: MutableList ) { val application = app.toApplication(context) - val filter = applicationDataManager.getAppFilterLevel(application, authData) + val filter = applicationDataManager.getAppFilterLevel(application) if (filter.isUnFiltered()) { applicationList.add( application.apply { @@ -173,7 +173,7 @@ class AppsApiImpl @Inject constructor( if (hasSingleResult()) { applicationList.add( apps[0].apply { - updateFilterLevel(null) + updateFilterLevel() } ) } @@ -201,7 +201,7 @@ class AppsApiImpl @Inject constructor( applicationDataManager.updateStatus(it) it.updateType() it.updateSource(context) - it.updateFilterLevel(authData) + it.updateFilterLevel() } application } @@ -217,7 +217,7 @@ class AppsApiImpl @Inject constructor( application: Application, authData: AuthData? ): FilterLevel { - return applicationDataManager.getAppFilterLevel(application, authData) + return applicationDataManager.getAppFilterLevel(application) } override fun isAnyFusedAppUpdated( diff --git a/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt index 07a84b5e9200b5954f17736478922e0ad7c8f972..cddc7f184f7b8e980da954569ea815f325ae4c0c 100644 --- a/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/category/CategoryApiImpl.kt @@ -21,7 +21,6 @@ package foundation.e.apps.data.application.category import android.content.Context import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.StreamCluster import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.AppSourcesContainer @@ -180,15 +179,13 @@ class CategoryApiImpl @Inject constructor( var nextPageUrl = "" return handleNetworkResult { - val streamCluster = - appSources.gplayRepo.getAppsByCategory(category, pageUrl) as StreamCluster + val cluster = + appSources.gplayRepo.getAppsByCategory(category, pageUrl) - val filteredAppList = filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) - filteredAppList.data?.let { - applicationList = it.toMutableList() - } + val filteredAppList = filterRestrictedGPlayApps(cluster.clusterAppList) + applicationList = (filteredAppList.data ?: emptyList()).toMutableList() - nextPageUrl = streamCluster.clusterNextPageUrl + nextPageUrl = cluster.clusterNextPageUrl if (nextPageUrl.isNotEmpty()) { applicationList.add(Application(isPlaceHolder = true)) } @@ -208,17 +205,12 @@ class CategoryApiImpl @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] */ private suspend fun filterRestrictedGPlayApps( - authData: AuthData, appList: List, ): ResultSupreme> { val filteredApplications = mutableListOf() return handleNetworkResult { appList.forEach { - val filter = applicationDataManager.getAppFilterLevel( - it.toApplication(context), - authData - ) - + val filter = applicationDataManager.getAppFilterLevel(it.toApplication(context)) if (filter.isUnFiltered()) { filteredApplications.add( it.toApplication(context).apply { @@ -242,7 +234,7 @@ class CategoryApiImpl @Inject constructor( response?.apps?.forEach { applicationDataManager.updateStatus(it) it.updateType() - applicationDataManager.updateFilterLevel(null, it) + applicationDataManager.updateFilterLevel(it) list.add(it) } } diff --git a/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt index 6ed63a29c8319ef3ef640269c5cec955f80bead3..d81e28a7fcc72d5fb7c9055d9a9345cb297b993d 100644 --- a/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt @@ -171,7 +171,7 @@ class HomeApiImpl @Inject constructor( val fusedApps = it.value.map { app -> app.apply { applicationDataManager.updateStatus(this) - applicationDataManager.updateFilterLevel(authData, this) + applicationDataManager.updateFilterLevel(this) } } list.add(Home(it.key, fusedApps)) diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index b040cd66242813d8b6da002fcce9b1470fa5fab7..9c2fa1356b2ed0c84b6da8098d01716ae1de04ac 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -37,17 +37,10 @@ import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.handleNetworkResult -import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.CleanApkIOException import foundation.e.apps.data.login.exceptions.GPlayIOException import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.utils.eventBus.AppEvent -import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -66,15 +59,6 @@ class SearchApiImpl @Inject constructor( @ApplicationContext lateinit var context: Context - companion object { - private const val KEYWORD_TEST_SEARCH = "facebook" - - private val DUMMY_SEARCH_EXPECTED_APPS = listOf( - "Facebook" to "com.facebook.katana", - "Messenger" to "com.facebook.orca" - ) - } - override fun getSelectedAppTypes(): List { val selectedAppTypes = mutableListOf() if (appLoungePreference.isGplaySelected()) selectedAppTypes.add(APP_TYPE_ANY) @@ -340,8 +324,6 @@ class SearchApiImpl @Inject constructor( nextPageSubBundle: Set? ): GplaySearchResult { val result = handleNetworkResult { - coroutineScope { launch(Dispatchers.IO) { doDummySearch() } } - val searchResults = appSources.gplayRepo.getSearchResult(query, nextPageSubBundle?.toMutableSet()) @@ -367,29 +349,6 @@ class SearchApiImpl @Inject constructor( ) } - // Initiate a dummy search to ensure Google Play returns enough results for the search query - private suspend fun doDummySearch() { - val (searchedApps, _) = appSources.gplayRepo.getSearchResult(KEYWORD_TEST_SEARCH, null) - - if (searchedApps.isEmpty()) { - Timber.d("Search returned empty results, refreshing token...") - refreshToken() - return - } - - val dummySearchPackageNames = DUMMY_SEARCH_EXPECTED_APPS.map { it.second } - val searchedAppsPackageNames = searchedApps.map { it.packageName } - - val isSearchContainingResults = - searchedAppsPackageNames.containsAll(dummySearchPackageNames) - - if (!isSearchContainingResults) { - Timber.d("Search didn't return enough results, refreshing token...") - refreshToken() - return - } - } - /* * This function will replace a GPlay app with F-Droid app if exists, * else will show the GPlay app itself. @@ -414,12 +373,4 @@ class SearchApiImpl @Inject constructor( return gPlayApps.map { it.toApplication(context) } } } - - private fun refreshToken() { - MainScope().launch { - EventBus.invokeEvent( - AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName) - ) - } - } } diff --git a/app/src/main/java/foundation/e/apps/data/faultyApps/FaultyAppDao.kt b/app/src/main/java/foundation/e/apps/data/faultyApps/FaultyAppDao.kt index d9fdc06a802c6ee48b06cf657950e99773af7c20..38451253d78381677385082110c615f4a118ed19 100644 --- a/app/src/main/java/foundation/e/apps/data/faultyApps/FaultyAppDao.kt +++ b/app/src/main/java/foundation/e/apps/data/faultyApps/FaultyAppDao.kt @@ -22,7 +22,7 @@ package foundation.e.apps.data.faultyApps import androidx.room.Dao import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE +import androidx.room.OnConflictStrategy.Companion.REPLACE import androidx.room.Query @Dao diff --git a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt index 2adef4c67b9cfcb4e2213c50aeda11985c774cde..757ea13a49c3d62df84c5db17d8543e211880c3e 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/GoogleLoginManager.kt @@ -64,7 +64,7 @@ class GoogleLoginManager( var authData: AuthData? withContext(Dispatchers.IO) { - authData = AuthHelper.build(email, aasToken, nativeDeviceProperty) + authData = AuthHelper.build(email, aasToken, tokenType = AuthHelper.Token.AAS, properties = nativeDeviceProperty) } return authData } diff --git a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt index 22e0d88b7d49db73fbd94e3e992eb6db0b5c8b5e..41d0ce4d3d1aa2959e12da9740e4126f87e1fda3 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt @@ -21,20 +21,21 @@ package foundation.e.apps.data.playstore import android.content.Context import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App as GplayApp -import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.Category import com.aurora.gplayapi.data.models.ContentRating import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.helpers.AppDetailsHelper -import com.aurora.gplayapi.helpers.CategoryAppsHelper -import com.aurora.gplayapi.helpers.CategoryHelper -import com.aurora.gplayapi.helpers.Chart import com.aurora.gplayapi.helpers.ContentRatingHelper import com.aurora.gplayapi.helpers.PurchaseHelper -import com.aurora.gplayapi.helpers.SearchHelper -import com.aurora.gplayapi.helpers.TopChartsHelper +import com.aurora.gplayapi.helpers.contracts.TopChartsContract.Chart +import com.aurora.gplayapi.helpers.contracts.TopChartsContract.Type +import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper +import com.aurora.gplayapi.helpers.web.WebCategoryHelper +import com.aurora.gplayapi.helpers.web.WebCategoryStreamHelper +import com.aurora.gplayapi.helpers.web.WebSearchHelper +import com.aurora.gplayapi.helpers.web.WebTopChartsHelper import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.StoreRepository @@ -57,14 +58,13 @@ class PlayStoreRepository @Inject constructor( override suspend fun getHomeScreenData(): Map> { val homeScreenData = mutableMapOf>() val homeElements = createTopChartElements() - val authData = authenticatorRepository.getGPlayAuthOrThrow() homeElements.forEach { if (it.value.isEmpty()) return@forEach val chart = it.value.keys.iterator().next() val type = it.value.values.iterator().next() - val result = getTopApps(type, chart, authData) + val result = getTopApps(type, chart) homeScreenData[it.key] = result } @@ -72,20 +72,19 @@ class PlayStoreRepository @Inject constructor( } private fun createTopChartElements() = mutableMapOf( - context.getString(R.string.topselling_free_apps) to mapOf(Chart.TOP_SELLING_FREE to TopChartsHelper.Type.APPLICATION), - context.getString(R.string.topselling_free_games) to mapOf(Chart.TOP_SELLING_FREE to TopChartsHelper.Type.GAME), - context.getString(R.string.topgrossing_apps) to mapOf(Chart.TOP_GROSSING to TopChartsHelper.Type.APPLICATION), - context.getString(R.string.topgrossing_games) to mapOf(Chart.TOP_GROSSING to TopChartsHelper.Type.GAME), - context.getString(R.string.movers_shakers_apps) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.APPLICATION), - context.getString(R.string.movers_shakers_games) to mapOf(Chart.MOVERS_SHAKERS to TopChartsHelper.Type.GAME), + context.getString(R.string.topselling_free_apps) to mapOf(Chart.TOP_SELLING_FREE to Type.APPLICATION), + context.getString(R.string.topselling_free_games) to mapOf(Chart.TOP_SELLING_FREE to Type.GAME), + context.getString(R.string.topgrossing_apps) to mapOf(Chart.TOP_GROSSING to Type.APPLICATION), + context.getString(R.string.topgrossing_games) to mapOf(Chart.TOP_GROSSING to Type.GAME), + context.getString(R.string.movers_shakers_apps) to mapOf(Chart.MOVERS_SHAKERS to Type.APPLICATION), + context.getString(R.string.movers_shakers_games) to mapOf(Chart.MOVERS_SHAKERS to Type.GAME), ) fun getSearchResult( query: String, subBundle: MutableSet? ): Pair, MutableSet> { - val authData = authenticatorRepository.getGPlayAuthOrThrow() - val searchHelper = SearchHelper(authData).using(gPlayHttpClient) + val searchHelper = WebSearchHelper().using(gPlayHttpClient) Timber.d("Fetching search result for $query, subBundle: $subBundle") @@ -109,49 +108,54 @@ class PlayStoreRepository @Inject constructor( } suspend fun getSearchSuggestions(query: String): List { - val authData = authenticatorRepository.getGPlayAuthOrThrow() - val searchData = mutableListOf() withContext(Dispatchers.IO) { - val searchHelper = SearchHelper(authData).using(gPlayHttpClient) + val searchHelper = WebSearchHelper().using(gPlayHttpClient) searchData.addAll(searchHelper.searchSuggestions(query)) } - return searchData.filter { it.suggestedQuery.isNotBlank() } + return searchData.filter { it.title.isNotBlank() } } fun getAppsByCategory(category: String, pageUrl: String?): StreamCluster { - val authData = authenticatorRepository.getGPlayAuthOrThrow() - - val subCategoryHelper = CategoryAppsHelper(authData).using(gPlayHttpClient) + val subCategoryHelper = WebCategoryStreamHelper().using(gPlayHttpClient) if (!pageUrl.isNullOrEmpty()) { - return subCategoryHelper.next(pageUrl) + return subCategoryHelper.nextStreamCluster(pageUrl) } - return subCategoryHelper.getCategoryAppsList(category.uppercase()) + val bundle = subCategoryHelper.fetch(upperCaseCategory(category)) + return bundle.streamClusters.entries.first().value } + private fun upperCaseCategory(path: String): String { + val lastPart = path.substringAfterLast("/").uppercase() + val basePath = path.substringBeforeLast("/") + return "$basePath/$lastPart" + } + suspend fun getCategories(type: CategoryType?): List { val categoryList = mutableListOf() if (type == null) { return categoryList } - val authData = authenticatorRepository.getGPlayAuthOrThrow() - withContext(Dispatchers.IO) { - val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) - categoryList.addAll(categoryHelper.getAllCategoriesList(getCategoryType(type))) + val categoryHelper = WebCategoryHelper().using(gPlayHttpClient) + categoryList.addAll(categoryHelper.getAllCategories(getCategoryType(type))) } return categoryList } override suspend fun getAppDetails(packageNameOrId: String): Application { var appDetails: GplayApp? - val authData = authenticatorRepository.getGPlayAuthOrThrow() + + val appDetailsHelper = try { + AppDetailsHelper(authenticatorRepository.getGPlayAuthOrThrow()).using(gPlayHttpClient) + } catch (exception: Exception) { + WebAppDetailsHelper().using(gPlayHttpClient) + } withContext(Dispatchers.IO) { - val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) appDetails = appDetailsHelper.getAppByPackageName(packageNameOrId) } @@ -160,12 +164,17 @@ class PlayStoreRepository @Inject constructor( suspend fun getAppsDetails(packageNamesOrIds: List): List { val appDetailsList = mutableListOf() - val authData = authenticatorRepository.getGPlayAuthOrThrow() + + val appDetailsHelper = try { + AppDetailsHelper(authenticatorRepository.getGPlayAuthOrThrow()).using(gPlayHttpClient) + } catch (exception: Exception) { + WebAppDetailsHelper().using(gPlayHttpClient) + } withContext(Dispatchers.IO) { - val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) appDetailsList.addAll(appDetailsHelper.getAppByPackageName(packageNamesOrIds)) } + return appDetailsList } @@ -175,14 +184,13 @@ class PlayStoreRepository @Inject constructor( } private suspend fun getTopApps( - type: TopChartsHelper.Type, - chart: Chart, - authData: AuthData + type: Type, + chart: Chart ): List { val topApps = mutableListOf() withContext(Dispatchers.IO) { - val topChartsHelper = TopChartsHelper(authData).using(gPlayHttpClient) - topApps.addAll(topChartsHelper.getCluster(type, chart).clusterAppList) + val topChartsHelper = WebTopChartsHelper().using(gPlayHttpClient) + topApps.addAll(topChartsHelper.getCluster(type.value, chart.value).clusterAppList) } return topApps.map { @@ -192,16 +200,28 @@ class PlayStoreRepository @Inject constructor( suspend fun getDownloadInfo( idOrPackageName: String, - versionCode: Any?, + versionCode: Int, offerType: Int ): List { val downloadData = mutableListOf() val authData = authenticatorRepository.getGPlayAuthOrThrow() + var version = versionCode + var offer = offerType + + if (version == 0) { + val appDetailsHelper = getAppDetails(idOrPackageName) + version = appDetailsHelper.latest_version_code + offer = appDetailsHelper.offer_type + } + + if (version == 0) { + throw IllegalStateException("Could not get download details for $idOrPackageName") + } + withContext(Dispatchers.IO) { - val version = versionCode?.let { it as Int } ?: -1 val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) - downloadData.addAll(purchaseHelper.purchase(idOrPackageName, version, offerType)) + downloadData.addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } return downloadData } @@ -218,7 +238,7 @@ class PlayStoreRepository @Inject constructor( withContext(Dispatchers.IO) { val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) downloadData.addAll( - purchaseHelper.getOnDemandModule(packageName, moduleName, versionCode, offerType) + purchaseHelper.purchase(packageName, versionCode, offerType, moduleName) ) } return downloadData diff --git a/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt b/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt index e2654d240c7749f3306b099aa8ca3926f995201f..4850ac7562c2bd5a43554a8ea5b46245f79e2c7d 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/utils/CustomAuthValidator.kt @@ -22,7 +22,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import com.aurora.gplayapi.data.providers.HeaderProvider import com.aurora.gplayapi.helpers.AuthValidator -import com.aurora.gplayapi.helpers.BaseHelper +import com.aurora.gplayapi.helpers.NativeHelper import com.aurora.gplayapi.network.IHttpClient /** @@ -33,7 +33,7 @@ import com.aurora.gplayapi.network.IHttpClient * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 */ -class CustomAuthValidator(authData: AuthData) : BaseHelper(authData) { +class CustomAuthValidator(authData: AuthData) : NativeHelper(authData) { override fun using(httpClient: IHttpClient) = apply { this.httpClient = httpClient diff --git a/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt b/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt index 0a948b9182eec163de0d37c3af7b571a944d6437..9ea2d6a1dcb5117e8c98e64a0dc984ec960257fc 100644 --- a/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt +++ b/app/src/main/java/foundation/e/apps/data/playstore/utils/GPlayHttpClient.kt @@ -44,6 +44,7 @@ import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber import java.io.IOException import java.net.SocketTimeoutException +import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -84,7 +85,7 @@ class GPlayHttpClient @Inject constructor( fun post(url: String, headers: Map, requestBody: RequestBody): PlayResponse { val request = Request.Builder() .url(url) - .headers(headers.toHeaders()) + .headers(headersWithLocale(headers).toHeaders()) .method(HTTP_METHOD_POST, requestBody) .build() return processRequest(request) @@ -98,8 +99,8 @@ class GPlayHttpClient @Inject constructor( ): PlayResponse { val request = Request.Builder() .url(buildUrl(url, params)) - .headers(headers.toHeaders()) - .method(HTTP_METHOD_POST, "".toRequestBody(null)) + .headers(headersWithLocale(headers).toHeaders()) + .method(HTTP_METHOD_POST, "".toRequestBody()) .build() return processRequest(request) } @@ -119,11 +120,7 @@ class GPlayHttpClient @Inject constructor( @Throws(IOException::class) override fun post(url: String, headers: Map, body: ByteArray): PlayResponse { - val requestBody = body.toRequestBody( - "application/x-protobuf".toMediaType(), - 0, - body.size - ) + val requestBody = body.toRequestBody() return post(url, headers, requestBody) } @@ -140,7 +137,7 @@ class GPlayHttpClient @Inject constructor( ): PlayResponse { val request = Request.Builder() .url(buildUrl(url, params)) - .headers(headers.toHeaders()) + .headers(headersWithLocale(headers).toHeaders()) .method(HTTP_METHOD_GET, null) .build() return processRequest(request) @@ -163,12 +160,18 @@ class GPlayHttpClient @Inject constructor( ): PlayResponse { val request = Request.Builder() .url(url + paramString) - .headers(headers.toHeaders()) + .headers(headersWithLocale(headers).toHeaders()) .method(HTTP_METHOD_GET, null) .build() return processRequest(request) } + private fun headersWithLocale(headers: Map): Map { + val headersWithLocale = headers.toMutableMap() + headersWithLocale["Accept-Language"] = Locale.getDefault().language + return headersWithLocale + } + private fun processRequest(request: Request): PlayResponse { // Reset response code as flow doesn't sends the same value twice _responseCode.value = 0 diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 505b95852ea506a4946076221b68a936c6e9d29e..8ba19b9c2d9cefce9e8a5f0e4e1541af4b48ec34 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -19,7 +19,7 @@ package foundation.e.apps.install.workmanager import android.content.Context -import com.aurora.gplayapi.exceptions.ApiException +import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme @@ -174,7 +174,7 @@ class AppInstallProcessor @Inject constructor( private suspend fun updateDownloadUrls(appInstall: AppInstall): Boolean { try { updateFusedDownloadWithAppDownloadLink(appInstall) - } catch (e: ApiException.AppNotPurchased) { + } catch (e: InternalException.AppNotPurchased) { appInstallComponents.appManagerWrapper.addFusedDownloadPurchaseNeeded(appInstall) EventBus.invokeEvent(AppEvent.AppPurchaseEvent(appInstall)) return false diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt index b897133a0e843325c1561710389ca1d108f12265..eb27c0ac6c0d3d78e9a8940b93de51f537103887 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivity.kt @@ -38,7 +38,7 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.setupWithNavController import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.exceptions.ApiException +import com.aurora.gplayapi.exceptions.InternalException import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -285,7 +285,7 @@ class MainActivity : AppCompatActivity() { private fun observeErrorMessage() { viewModel.errorMessage.observe(this) { when (it) { - is ApiException.AppNotPurchased -> showSnackbarMessage(getString(R.string.message_app_available_later)) + is InternalException.AppNotPurchased -> showSnackbarMessage(getString(R.string.message_app_available_later)) else -> showSnackbarMessage( it.localizedMessage ?: getString(R.string.unknown_error) ) diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt index c4bde026cb236c9f42b91deebeee464b6dffdb49..3c1a5750538a1dba1b6a1bb4c2f3ead1bf7c0b0b 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt @@ -490,10 +490,14 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } catText = catText.replace("_", " ") - categoryTitle.text = catText + categoryTitle.text = catText.capitalizeFirstLetter() } } + private fun String.capitalizeFirstLetter(): String { + return this.lowercase().replaceFirstChar { it.uppercase() } + } + private fun setupScreenshotRVAdapter() { screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) binding.recyclerView.apply { diff --git a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt index 3631984268053a4273c9cdadc5130a43ee5ba19d..2afc7f4824da6454b75d7a985713cf1ab2db2a6e 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt @@ -22,7 +22,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.ContentRating -import com.aurora.gplayapi.exceptions.ApiException +import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.R import foundation.e.apps.data.application.ApplicationRepository @@ -164,7 +164,7 @@ class ApplicationViewModel @Inject constructor( exceptionsList.add(exception) exceptionsLiveData.postValue(exceptionsList) } - } catch (e: ApiException.AppNotFound) { + } catch (e: InternalException.AppNotFound) { _errorMessageLiveData.postValue(R.string.app_not_found) } catch (e: Exception) { _errorMessageLiveData.postValue(R.string.unknown_error) diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt index 7c5e7c9fc4cb233c1b841545508dfc4c6375c3f3..68d6d50dff47a472ddaf41bc235393bd2317d9ff 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt @@ -258,7 +258,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), ApplicationInstall override fun onResume() { super.onResume() - binding.shimmerLayout.startShimmer() + showLoadingUI() appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { updateProgressOfDownloadingAppItemViews(homeParentRVAdapter, it) } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index c84b019d8050b474cba08c1ca4e836a7ecdca9ce..f9017ec0618176c23ced6cb11941c71ceda27012 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -235,7 +235,7 @@ class SearchFragment : } showData() - listAdapter?.setData(appList) + listAdapter?.setData(appList.filter { it.name.isNotBlank() }) return true } @@ -477,7 +477,7 @@ class SearchFragment : override fun onSuggestionClick(position: Int): Boolean { searchViewModel.searchSuggest.value?.let { if (it.isNotEmpty()) { - searchView?.setQuery(it[position].suggestedQuery, true) + searchView?.setQuery(it[position].title, true) } } return true @@ -523,7 +523,7 @@ class SearchFragment : val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) suggestions?.let { for (i in it.indices) { - cursor.addRow(arrayOf(i, it[i].suggestedQuery)) + cursor.addRow(arrayOf(i, it[i].title)) } } searchView?.suggestionsAdapter?.changeCursor(cursor) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index f4562656a69bc54945e4279db4e9b33673019f5e..9ee353e58272cd304ae1d49b7b08e0c36d91e99e 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -97,10 +97,9 @@ class SearchViewModel @Inject constructor( fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { viewModelScope.launch(Dispatchers.IO) { - if (gPlayAuth.result.isSuccess()) - searchSuggest.postValue( - applicationRepository.getSearchSuggestions(query) - ) + searchSuggest.postValue( + applicationRepository.getSearchSuggestions(query) + ) } } @@ -110,11 +109,10 @@ class SearchViewModel @Inject constructor( authObjectList: List, retryBlock: (failedObjects: List) -> Boolean ) { - if (query.isBlank()) return this.lastAuthObjects = authObjectList - super.onLoadData(authObjectList, { successAuthList, _ -> + super.onLoadData(authObjectList, { successAuthList, failedAuthList -> successAuthList.find { it is AuthObject.CleanApk }?.run { fetchCleanApkData(query, null) } @@ -124,6 +122,11 @@ class SearchViewModel @Inject constructor( fetchGplayData(query) } + failedAuthList.find { it is AuthObject.GPlayAuth }?.run { + nextSubBundle = null + fetchGplayData(query) + } + }, retryBlock) } diff --git a/app/src/test/java/foundation/e/apps/fused/SearchApiImplTest.kt b/app/src/test/java/foundation/e/apps/fused/SearchApiImplTest.kt index 3bb56cea6c3ac1584290c6e5dc02cdcac6167195..241d87a56215eb6076c14f3aba546c5bd645aaee 100644 --- a/app/src/test/java/foundation/e/apps/fused/SearchApiImplTest.kt +++ b/app/src/test/java/foundation/e/apps/fused/SearchApiImplTest.kt @@ -286,26 +286,4 @@ class SearchApiImplTest { val size = searchResultLiveData.data?.first?.size ?: -2 assertEquals("getSearchResult", 4, size) } - - @Test - fun testSearchResultWhenDataIsLimited() = runTest { - preferenceManagerModule.isGplaySelectedFake = true - formatterMocked.`when` { Formatter.formatFileSize(any(), any()) }.thenReturn("15MB") - Mockito.`when`(gPlayAPIRepository.getSearchResult(anyString(), eq(null))) - .thenReturn(Pair(emptyList(), mutableSetOf())) - Mockito.`when`(cleanApkAppsRepository.getAppDetails(any())).thenReturn(null) - - var isEventBusTriggered = false - val job = launch { - EventBus.events.collect { - isEventBusTriggered = true - } - } - - fusedAPIImpl.getGplaySearchResult("anything", null) - delay(500) - job.cancel() - - assert(isEventBusTriggered) - } } diff --git a/build.gradle b/build.gradle index d1c2c788a66d9506701ba423214eb52bf022190f..f5341756b1d8d174074f2725f454a8261a5c4a61 100644 --- a/build.gradle +++ b/build.gradle @@ -3,12 +3,12 @@ plugins { id 'com.android.application' version '8.1.0' apply false id 'com.android.library' version '8.1.0' apply false - id 'org.jetbrains.kotlin.android' version '1.8.0' apply false - id 'com.google.dagger.hilt.android' version '2.44' apply false - id "org.jetbrains.kotlin.plugin.allopen" version "1.8.0" + id 'org.jetbrains.kotlin.android' version '2.0.21' apply false + id "org.jetbrains.kotlin.plugin.allopen" version "2.0.21" + id 'org.jetbrains.kotlin.jvm' version '2.0.21' apply false + id 'com.google.dagger.hilt.android' version '2.51.1' apply false id 'androidx.navigation.safeargs' version '2.5.3' apply false id 'io.gitlab.arturbosch.detekt' version '1.23.1' - id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false } allprojects {