From 2c65f6043b8cc939c7558f04328b09361132f951 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 13 Jun 2025 12:57:31 +0600 Subject: [PATCH 1/2] refactor: remove cache from GPlayHttpClient When a limited response is sent by GPlay API, it's cached and served for subsequent network calls. Hence, the details response couldn't be fetch to proceed with the installation. Removing the cache forces the OkHttpClient for GPlay to retrieve new response every time a new request is made. --- .../e/apps/data/playstore/utils/GPlayHttpClient.kt | 4 ---- .../{GplyHttpClientTest.kt => GPlayHttpClientTest.kt} | 8 ++------ 2 files changed, 2 insertions(+), 10 deletions(-) rename app/src/test/java/foundation/e/apps/gplay/{GplyHttpClientTest.kt => GPlayHttpClientTest.kt} (97%) 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 9ea2d6a1d..d3570cdc3 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 @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import okhttp3.Cache import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @@ -49,7 +48,6 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject class GPlayHttpClient @Inject constructor( - private val cache: Cache, loggingInterceptor: HttpLoggingInterceptor ) : IHttpClient { @@ -77,7 +75,6 @@ class GPlayHttpClient @Inject constructor( .callTimeout(HTTP_TIMEOUT_IN_SECOND, TimeUnit.SECONDS) .followRedirects(true) .followSslRedirects(true) - .cache(cache) .addInterceptor(loggingInterceptor) .build() @@ -212,7 +209,6 @@ class GPlayHttpClient @Inject constructor( } STATUS_CODE_TOO_MANY_REQUESTS -> MainScope().launch { - cache.evictAll() if (url.toString().contains(SEARCH_SUGGEST)) { return@launch } diff --git a/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt b/app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt similarity index 97% rename from app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt rename to app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt index 3ebcf2e29..267264a52 100644 --- a/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt +++ b/app/src/test/java/foundation/e/apps/gplay/GPlayHttpClientTest.kt @@ -32,7 +32,6 @@ import io.mockk.mockkObject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest -import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor @@ -48,10 +47,7 @@ import org.mockito.kotlin.any import kotlin.test.assertFailsWith @OptIn(ExperimentalCoroutinesApi::class) -class GplyHttpClientTest { - - @Mock - private lateinit var cache: Cache +class GPlayHttpClientTest { @Mock private lateinit var loggingInterceptor: HttpLoggingInterceptor @@ -70,7 +66,7 @@ class GplyHttpClientTest { @Before fun setup() { MockitoAnnotations.openMocks(this) - gPlayHttpClient = GPlayHttpClient(cache, loggingInterceptor) + gPlayHttpClient = GPlayHttpClient(loggingInterceptor) gPlayHttpClient.okHttpClient = this.okHttpClient call = FakeCall() } -- GitLab From 5ee74ab7900febd87f592cac3473cc0534969b22 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Fri, 13 Jun 2025 12:07:37 +0600 Subject: [PATCH 2/2] fix: refresh token before PlayStore app installation when a limited response is returned from GPlay When users try to install an app from the Home, Search or Categories screens, the API calling switches from web-based to auth-based. By that time, if the token expires in the background, Google returns a limited response (version code being 0) because of the stale token, which is inadequate to install the app. To fix this, the token is refreshed and app details are fetched again with the refreshed token. Even after refreshing, if the version code is still 0, then it's not possible to further proceed to installation from there and an exception is thrown. --- .../data/playstore/PlayStoreRepository.kt | 55 ++++++++++++------- .../workmanager/AppInstallProcessor.kt | 2 +- 2 files changed, 37 insertions(+), 20 deletions(-) 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 aaa672d59..f8d1968cc 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 @@ -46,6 +46,7 @@ import foundation.e.apps.data.application.utils.toApplication import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.AuthenticatorRepository +import foundation.e.apps.data.login.PlayStoreAuthenticator import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.utils.SystemInfoProvider import kotlinx.coroutines.Dispatchers @@ -144,21 +145,39 @@ class PlayStoreRepository @Inject constructor( return categoryList } - override suspend fun getAppDetails(packageName: String): Application { - var appDetails: GplayApp? + override suspend fun getAppDetails(packageName: String): Application = + withContext(Dispatchers.IO) { + var appDetails: GplayApp + appDetails = getAppDetailsHelper().getAppByPackageName(packageName) - val appDetailsHelper = - AppDetailsHelper(authenticatorRepository.getGPlayAuthOrThrow()).using(gPlayHttpClient) + if (!isEmulator() && appDetails.versionCode == 0) { + // Google Play returns limited result ( i.e. version code being 0) with a stale token, + // so we need to refresh authentication to get a new token. + Timber.i("Version code is 0 for ${appDetails.packageName}.") - withContext(Dispatchers.IO) { - appDetails = appDetailsHelper.getAppByPackageName(packageName) - } + refreshPlayStoreAuthentication() + + appDetails = getAppDetailsHelper().getAppByPackageName(packageName) + + if (appDetails.versionCode == 0) { + Timber.w("After refreshing auth, version code is still 0. Giving up installation.") + throw IllegalStateException("App version code cannot be 0") + } + } - if (!isEmulator() && appDetails?.versionCode == 0) { - throw IllegalStateException("App version code cannot be 0") + appDetails.toApplication(context) } - return appDetails?.toApplication(context) ?: Application() + private fun getAppDetailsHelper(): AppDetailsHelper { + val authData = authenticatorRepository.getGPlayAuthOrThrow() + val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) + + return appDetailsHelper + } + + private suspend fun refreshPlayStoreAuthentication() { + Timber.i("Refreshing authentication.") + authenticatorRepository.getAuthObjects(listOf(PlayStoreAuthenticator::class.java.simpleName)) } suspend fun getAppDetailsWeb(packageName: String): Application? { @@ -206,10 +225,7 @@ class PlayStoreRepository @Inject constructor( idOrPackageName: String, versionCode: Int, offerType: Int - ): List { - val downloadData = mutableListOf() - val authData = authenticatorRepository.getGPlayAuthOrThrow() - + ): List = withContext(Dispatchers.IO) { var version = versionCode var offer = offerType @@ -223,11 +239,12 @@ class PlayStoreRepository @Inject constructor( throw IllegalStateException("Could not get download details for $idOrPackageName") } - withContext(Dispatchers.IO) { - val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) - downloadData.addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) - } - return downloadData + // Don't store auth data in a variable. Always get GPlay auth from repository, + // because auth might get refreshed while fetching app details. + val purchaseHelper = PurchaseHelper(authenticatorRepository.getGPlayAuthOrThrow()) + .using(gPlayHttpClient) + + buildList { addAll(purchaseHelper.purchase(idOrPackageName, version, offer)) } } suspend fun getOnDemandModule( 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 120a3a0a6..4646bf896 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 @@ -206,7 +206,7 @@ class AppInstallProcessor @Inject constructor( ) return false } catch (e: IllegalStateException) { - EventBus.invokeEvent(AppEvent.InvalidAuthEvent(AuthObject.GPlayAuth::class.java.simpleName)) + Timber.e(e) } catch (e: Exception) { handleUpdateDownloadError( appInstall, -- GitLab