Loading app/src/main/java/foundation/e/apps/data/login/playstore/MicrogAccountManager.kt→app/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +42 −15 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ * */ package foundation.e.apps.data.login.playstore package foundation.e.apps.data.login.microg import android.accounts.Account import android.accounts.AccountManager Loading @@ -29,7 +29,6 @@ import android.util.Base64 import androidx.core.os.BundleCompat import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.microg.MicrogCertUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject Loading @@ -41,10 +40,10 @@ data class MicrogAccount( ) @Singleton class MicrogAccountManager @Inject constructor( class MicrogLoginManager @Inject constructor( @ApplicationContext val context: Context, private val accountManager: AccountManager ) : LoginManager<MicrogAccountManager.FetchResult> { ) : LoginManager<MicrogLoginManager.FetchResult> { sealed interface FetchResult { data class Success(val microgAccount: MicrogAccount) : FetchResult data class RequiresUserAction(val intent: Intent) : FetchResult Loading @@ -58,8 +57,9 @@ class MicrogAccountManager @Inject constructor( override suspend fun login(): FetchResult { return fetchMicrogAccount() } suspend fun fetchMicrogAccount( accountName: String? = null accountName: String = "" ): FetchResult = withContext(Dispatchers.IO) { val accounts = accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) if (accounts.isEmpty()) { Loading @@ -68,21 +68,14 @@ class MicrogAccountManager @Inject constructor( ) } val account = accountName?.let { name -> accounts.firstOrNull { it.name == name } } ?: accounts.first() val account = resolveAccount(accounts, accountName) return@withContext runCatching { val options = createAuthTokenOptions() val bundle = accountManager.getAuthToken( account, MicrogCertUtil.PLAY_AUTH_SCOPE, Bundle().apply { putString("overridePackage", MicrogCertUtil.GOOGLE_PLAY_PACKAGE) putByteArray( "overrideCertificate", Base64.decode(MicrogCertUtil.GOOGLE_PLAY_CERT_BASE64, Base64.DEFAULT) ) }, options, false, null, null Loading @@ -104,6 +97,40 @@ class MicrogAccountManager @Inject constructor( } } suspend fun invalidateAuthToken(accountName: String, oldToken: String) { if (oldToken.isBlank()) { return } withContext(Dispatchers.IO) { val accounts = accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) if (accounts.isNotEmpty()) { val account = resolveAccount(accounts, accountName) accountManager.invalidateAuthToken(account.type, oldToken) } } } private fun resolveAccount(accounts: Array<Account>, accountName: String): Account { return if (accountName.isNotBlank()) { accounts.firstOrNull { it.name == accountName } ?: accounts.first() } else { accounts.first() } } private fun createAuthTokenOptions(): Bundle { return Bundle().apply { putString("overridePackage", MicrogCertUtil.GOOGLE_PLAY_PACKAGE) putByteArray( "overrideCertificate", Base64.decode(MicrogCertUtil.GOOGLE_PLAY_CERT_BASE64, Base64.DEFAULT) ) } } private fun <T : Parcelable> Bundle.parcelableCompat(key: String, clazz: Class<T>): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getParcelable(key, clazz) Loading app/src/main/java/foundation/e/apps/data/login/microg/MicrogTokenRefresher.kt 0 → 100644 +71 −0 Original line number Diff line number Diff line package foundation.e.apps.data.login.microg import foundation.e.apps.data.enums.User import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import javax.inject.Inject import javax.inject.Singleton @Singleton class MicrogTokenRefresher @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore, private val microgLoginManager: MicrogLoginManager ) { suspend fun refreshIfNeeded(user: User): Boolean { val hasMicrogGoogleAccount = hasMicrogGoogleAccount(user) val oldToken = readOldToken(hasMicrogGoogleAccount) val shouldRefresh = shouldRefreshToken(hasMicrogGoogleAccount, oldToken) if (!shouldRefresh) { return true } val accountName = appLoungeDataStore.emailData.getSync().ifBlank { "" } return refreshToken(accountName, oldToken) } private suspend fun refreshToken(accountName: String, oldToken: String): Boolean { microgLoginManager.invalidateAuthToken(accountName, oldToken) val result = microgLoginManager.fetchMicrogAccount(accountName) return handleRefreshResult(result) } private suspend fun handleRefreshResult(result: MicrogLoginManager.FetchResult): Boolean { return when (result) { is MicrogLoginManager.FetchResult.Success -> { updateStoredTokens(result.microgAccount) true } is MicrogLoginManager.FetchResult.RequiresUserAction -> false is MicrogLoginManager.FetchResult.Error -> false } } private suspend fun updateStoredTokens(microgAccount: MicrogAccount) { appLoungeDataStore.saveGoogleLogin( microgAccount.account.name, microgAccount.oauthToken ) appLoungeDataStore.saveAasToken("") } private fun hasMicrogGoogleAccount(user: User): Boolean { return user == User.GOOGLE && microgLoginManager.hasMicrogAccount() } private fun readOldToken(hasMicrogGoogleAccount: Boolean): String { return if (hasMicrogGoogleAccount) { appLoungeDataStore.oauthToken.getSync() } else { "" } } private fun shouldRefreshToken(hasMicrogGoogleAccount: Boolean, oldToken: String): Boolean { return hasMicrogGoogleAccount && oldToken.startsWith(MICROG_TOKEN_PREFIX) } private companion object { private const val MICROG_TOKEN_PREFIX = "ya29." } } app/src/main/java/foundation/e/apps/data/login/playstore/PlayStoreAuthDataRefresher.kt +62 −19 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.microg.MicrogTokenRefresher import foundation.e.apps.data.preference.AppLoungeDataStore import timber.log.Timber import java.util.Locale Loading @@ -41,46 +42,88 @@ class PlayStoreAuthDataRefresher @Inject constructor( private val googleLoginManager: GoogleLoginManager, private val anonymousLoginManager: AnonymousLoginManager, private val authDataProviderFactory: PlayStoreAuthDataProviderFactory, private val microgAuthTokenRefresher: MicrogTokenRefresher, private val userProfileFetcher: UserProfileFetcher, ) { private val user: User get() = appLoungeDataStore.getUser() private val locale: Locale get() = context.resources.configuration.locales[0] private val loginManager: LoginManager<AuthData?> get() = if (user == User.GOOGLE) { private fun loginManagerFor(user: User): LoginManager<AuthData?> { return if (user == User.GOOGLE) { googleLoginManager } else { anonymousLoginManager } } private val locale: Locale get() = context.resources.configuration.locales[0] private val session: PlayStoreSession get() = PlayStoreSession(loginManager, user, locale) private fun sessionFor(user: User): PlayStoreSession { return PlayStoreSession(loginManagerFor(user), user, locale) } suspend fun validateAuthData(): ResultSupreme<AuthData?> { val user = appLoungeDataStore.getUser() val savedAuth = authDataCache.getSavedAuthData() if (isAuthDataValid(savedAuth)) { return ResultSupreme.create(ResultStatus.OK, savedAuth) } Timber.i("Validating AuthData...") return refreshAuthData() return refreshAuthData(user) } private suspend fun isAuthDataValid(savedAuth: AuthData?) = savedAuth != null && authDataVerifier.validate(savedAuth).isSuccessful private suspend fun refreshAuthData(): ResultSupreme<AuthData?> { val authData = fetchAuthDataForUserType() val data = authData.data ?: return ResultSupreme.create(ResultStatus.UNKNOWN) authDataCache.saveAuthData(data) private suspend fun refreshAuthData( user: User ): ResultSupreme<AuthData?> { val microgRefreshSuccessful = microgAuthTokenRefresher.refreshIfNeeded(user) val authDataResult = if (microgRefreshSuccessful) { fetchAuthDataForUserType(user) } else { ResultSupreme.Error("MicroG refresh failed") } val refreshedAuth = authDataResult.data ?: return authDataResult val profileAuth = applyUserProfile(user, refreshedAuth) if (profileAuth != authDataResult.data) { authDataResult.setData(profileAuth) } if (microgRefreshSuccessful) { authDataCache.saveAuthData(profileAuth) } return authDataResult } private suspend fun applyUserProfile(user: User, authData: AuthData): AuthData { if (user != User.GOOGLE) { return authData } private suspend fun fetchAuthDataForUserType(): ResultSupreme<AuthData?> { val provider = authDataProviderFactory.create(appLoungeDataStore.getUser(), session) ?: return ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") return provider.fetch() val fetchedProfile = userProfileFetcher.fetch(authData) return if (fetchedProfile == null) { authData } else { authData.copy(userProfile = fetchedProfile) } } private suspend fun fetchAuthDataForUserType(user: User): ResultSupreme<AuthData?> { val provider = authDataProviderFactory.create(user, sessionFor(user)) return if (provider == null) { ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") } else { val result = provider.fetch() val authData = result.data if (authData == null) { ResultSupreme.Error(result.message, result.exception) } else { ResultSupreme.replicate(result, authData) } } } } app/src/main/java/foundation/e/apps/data/login/playstore/UserProfileFetcher.kt 0 → 100644 +39 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.login.playstore import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.UserProfile import com.aurora.gplayapi.helpers.UserProfileHelper import foundation.e.apps.data.playstore.utils.GPlayHttpClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class UserProfileFetcher @Inject constructor( private val gPlayHttpClient: GPlayHttpClient ) { suspend fun fetch(authData: AuthData): UserProfile? = withContext(Dispatchers.IO) { runCatching { UserProfileHelper(authData).using(gPlayHttpClient).getUserProfile() }.getOrNull() } } app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +38 −5 Original line number Diff line number Diff line Loading @@ -159,7 +159,9 @@ class PlayStoreRepository @Inject constructor( // batch request response might not contain images and descriptions of the app suspend fun getAppsDetails(packageNames: List<String>): List<Application> = withContext(Dispatchers.IO) { var appDetails: List<GplayApp> = getAppDetailsHelper().getAppByPackageName(packageNames) var appDetails = retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageNames) } if (!isEmulator() && appDetails.all { it.versionCode == 0L } && isAnonymousUser()) { // Google Play returns limited result ( i.e. version code being 0) with a stale token, Loading @@ -168,7 +170,9 @@ class PlayStoreRepository @Inject constructor( refreshPlayStoreAuthentication() appDetails = getAppDetailsHelper().getAppByPackageName(packageNames) appDetails = retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageNames) } if (appDetails.all { it.versionCode == 0L }) { Timber.w("After refreshing auth, version code is still 0.") Loading @@ -181,8 +185,10 @@ class PlayStoreRepository @Inject constructor( override suspend fun getAppDetails(packageName: String): Application = withContext(Dispatchers.IO) { var appDetails: GplayApp = try { var appDetails = try { retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageName) } } catch (exception: GplayHttpRequestException) { if (exception.status == HttpURLConnection.HTTP_NOT_FOUND) { throw InternalException.AppNotFound() Loading @@ -197,7 +203,9 @@ class PlayStoreRepository @Inject constructor( refreshPlayStoreAuthentication() appDetails = getAppDetailsHelper().getAppByPackageName(packageName) appDetails = retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageName) } if (appDetails.versionCode == 0L) { Timber.w("After refreshing auth, version code is still 0. Giving up installation.") Loading @@ -220,6 +228,31 @@ class PlayStoreRepository @Inject constructor( authenticatorRepository.fetchAuthObjects(listOf(StoreType.PLAY_STORE)) } private suspend fun <T> retryOnUnauthorized(block: suspend () -> T): T { return try { block() } catch (exception: Exception) { val isUnauthorized: Boolean = when (exception) { is GplayHttpRequestException -> exception.status == HttpURLConnection.HTTP_UNAUTHORIZED is InternalException.AppNotFound -> gPlayHttpClient.responseCode.value == HttpURLConnection.HTTP_UNAUTHORIZED else -> false } if (!isUnauthorized) { throw exception } val refreshResult = authenticatorRepository.getValidatedAuthData() val isSuccess: Boolean = refreshResult.isSuccess() && refreshResult.data != null if (!isSuccess) { throw exception } block() } } suspend fun getAppDetailsWeb(packageName: String): Application? { val webAppDetailsHelper = WebAppDetailsHelper().using(gPlayHttpClient) Loading Loading
app/src/main/java/foundation/e/apps/data/login/playstore/MicrogAccountManager.kt→app/src/main/java/foundation/e/apps/data/login/microg/MicrogLoginManager.kt +42 −15 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ * */ package foundation.e.apps.data.login.playstore package foundation.e.apps.data.login.microg import android.accounts.Account import android.accounts.AccountManager Loading @@ -29,7 +29,6 @@ import android.util.Base64 import androidx.core.os.BundleCompat import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.microg.MicrogCertUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject Loading @@ -41,10 +40,10 @@ data class MicrogAccount( ) @Singleton class MicrogAccountManager @Inject constructor( class MicrogLoginManager @Inject constructor( @ApplicationContext val context: Context, private val accountManager: AccountManager ) : LoginManager<MicrogAccountManager.FetchResult> { ) : LoginManager<MicrogLoginManager.FetchResult> { sealed interface FetchResult { data class Success(val microgAccount: MicrogAccount) : FetchResult data class RequiresUserAction(val intent: Intent) : FetchResult Loading @@ -58,8 +57,9 @@ class MicrogAccountManager @Inject constructor( override suspend fun login(): FetchResult { return fetchMicrogAccount() } suspend fun fetchMicrogAccount( accountName: String? = null accountName: String = "" ): FetchResult = withContext(Dispatchers.IO) { val accounts = accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) if (accounts.isEmpty()) { Loading @@ -68,21 +68,14 @@ class MicrogAccountManager @Inject constructor( ) } val account = accountName?.let { name -> accounts.firstOrNull { it.name == name } } ?: accounts.first() val account = resolveAccount(accounts, accountName) return@withContext runCatching { val options = createAuthTokenOptions() val bundle = accountManager.getAuthToken( account, MicrogCertUtil.PLAY_AUTH_SCOPE, Bundle().apply { putString("overridePackage", MicrogCertUtil.GOOGLE_PLAY_PACKAGE) putByteArray( "overrideCertificate", Base64.decode(MicrogCertUtil.GOOGLE_PLAY_CERT_BASE64, Base64.DEFAULT) ) }, options, false, null, null Loading @@ -104,6 +97,40 @@ class MicrogAccountManager @Inject constructor( } } suspend fun invalidateAuthToken(accountName: String, oldToken: String) { if (oldToken.isBlank()) { return } withContext(Dispatchers.IO) { val accounts = accountManager.getAccountsByType(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE) if (accounts.isNotEmpty()) { val account = resolveAccount(accounts, accountName) accountManager.invalidateAuthToken(account.type, oldToken) } } } private fun resolveAccount(accounts: Array<Account>, accountName: String): Account { return if (accountName.isNotBlank()) { accounts.firstOrNull { it.name == accountName } ?: accounts.first() } else { accounts.first() } } private fun createAuthTokenOptions(): Bundle { return Bundle().apply { putString("overridePackage", MicrogCertUtil.GOOGLE_PLAY_PACKAGE) putByteArray( "overrideCertificate", Base64.decode(MicrogCertUtil.GOOGLE_PLAY_CERT_BASE64, Base64.DEFAULT) ) } } private fun <T : Parcelable> Bundle.parcelableCompat(key: String, clazz: Class<T>): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { getParcelable(key, clazz) Loading
app/src/main/java/foundation/e/apps/data/login/microg/MicrogTokenRefresher.kt 0 → 100644 +71 −0 Original line number Diff line number Diff line package foundation.e.apps.data.login.microg import foundation.e.apps.data.enums.User import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.preference.getSync import javax.inject.Inject import javax.inject.Singleton @Singleton class MicrogTokenRefresher @Inject constructor( private val appLoungeDataStore: AppLoungeDataStore, private val microgLoginManager: MicrogLoginManager ) { suspend fun refreshIfNeeded(user: User): Boolean { val hasMicrogGoogleAccount = hasMicrogGoogleAccount(user) val oldToken = readOldToken(hasMicrogGoogleAccount) val shouldRefresh = shouldRefreshToken(hasMicrogGoogleAccount, oldToken) if (!shouldRefresh) { return true } val accountName = appLoungeDataStore.emailData.getSync().ifBlank { "" } return refreshToken(accountName, oldToken) } private suspend fun refreshToken(accountName: String, oldToken: String): Boolean { microgLoginManager.invalidateAuthToken(accountName, oldToken) val result = microgLoginManager.fetchMicrogAccount(accountName) return handleRefreshResult(result) } private suspend fun handleRefreshResult(result: MicrogLoginManager.FetchResult): Boolean { return when (result) { is MicrogLoginManager.FetchResult.Success -> { updateStoredTokens(result.microgAccount) true } is MicrogLoginManager.FetchResult.RequiresUserAction -> false is MicrogLoginManager.FetchResult.Error -> false } } private suspend fun updateStoredTokens(microgAccount: MicrogAccount) { appLoungeDataStore.saveGoogleLogin( microgAccount.account.name, microgAccount.oauthToken ) appLoungeDataStore.saveAasToken("") } private fun hasMicrogGoogleAccount(user: User): Boolean { return user == User.GOOGLE && microgLoginManager.hasMicrogAccount() } private fun readOldToken(hasMicrogGoogleAccount: Boolean): String { return if (hasMicrogGoogleAccount) { appLoungeDataStore.oauthToken.getSync() } else { "" } } private fun shouldRefreshToken(hasMicrogGoogleAccount: Boolean, oldToken: String): Boolean { return hasMicrogGoogleAccount && oldToken.startsWith(MICROG_TOKEN_PREFIX) } private companion object { private const val MICROG_TOKEN_PREFIX = "ya29." } }
app/src/main/java/foundation/e/apps/data/login/playstore/PlayStoreAuthDataRefresher.kt +62 −19 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.api.LoginManager import foundation.e.apps.data.login.microg.MicrogTokenRefresher import foundation.e.apps.data.preference.AppLoungeDataStore import timber.log.Timber import java.util.Locale Loading @@ -41,46 +42,88 @@ class PlayStoreAuthDataRefresher @Inject constructor( private val googleLoginManager: GoogleLoginManager, private val anonymousLoginManager: AnonymousLoginManager, private val authDataProviderFactory: PlayStoreAuthDataProviderFactory, private val microgAuthTokenRefresher: MicrogTokenRefresher, private val userProfileFetcher: UserProfileFetcher, ) { private val user: User get() = appLoungeDataStore.getUser() private val locale: Locale get() = context.resources.configuration.locales[0] private val loginManager: LoginManager<AuthData?> get() = if (user == User.GOOGLE) { private fun loginManagerFor(user: User): LoginManager<AuthData?> { return if (user == User.GOOGLE) { googleLoginManager } else { anonymousLoginManager } } private val locale: Locale get() = context.resources.configuration.locales[0] private val session: PlayStoreSession get() = PlayStoreSession(loginManager, user, locale) private fun sessionFor(user: User): PlayStoreSession { return PlayStoreSession(loginManagerFor(user), user, locale) } suspend fun validateAuthData(): ResultSupreme<AuthData?> { val user = appLoungeDataStore.getUser() val savedAuth = authDataCache.getSavedAuthData() if (isAuthDataValid(savedAuth)) { return ResultSupreme.create(ResultStatus.OK, savedAuth) } Timber.i("Validating AuthData...") return refreshAuthData() return refreshAuthData(user) } private suspend fun isAuthDataValid(savedAuth: AuthData?) = savedAuth != null && authDataVerifier.validate(savedAuth).isSuccessful private suspend fun refreshAuthData(): ResultSupreme<AuthData?> { val authData = fetchAuthDataForUserType() val data = authData.data ?: return ResultSupreme.create(ResultStatus.UNKNOWN) authDataCache.saveAuthData(data) private suspend fun refreshAuthData( user: User ): ResultSupreme<AuthData?> { val microgRefreshSuccessful = microgAuthTokenRefresher.refreshIfNeeded(user) val authDataResult = if (microgRefreshSuccessful) { fetchAuthDataForUserType(user) } else { ResultSupreme.Error("MicroG refresh failed") } val refreshedAuth = authDataResult.data ?: return authDataResult val profileAuth = applyUserProfile(user, refreshedAuth) if (profileAuth != authDataResult.data) { authDataResult.setData(profileAuth) } if (microgRefreshSuccessful) { authDataCache.saveAuthData(profileAuth) } return authDataResult } private suspend fun applyUserProfile(user: User, authData: AuthData): AuthData { if (user != User.GOOGLE) { return authData } private suspend fun fetchAuthDataForUserType(): ResultSupreme<AuthData?> { val provider = authDataProviderFactory.create(appLoungeDataStore.getUser(), session) ?: return ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") return provider.fetch() val fetchedProfile = userProfileFetcher.fetch(authData) return if (fetchedProfile == null) { authData } else { authData.copy(userProfile = fetchedProfile) } } private suspend fun fetchAuthDataForUserType(user: User): ResultSupreme<AuthData?> { val provider = authDataProviderFactory.create(user, sessionFor(user)) return if (provider == null) { ResultSupreme.Error("User type not ANONYMOUS or GOOGLE") } else { val result = provider.fetch() val authData = result.data if (authData == null) { ResultSupreme.Error(result.message, result.exception) } else { ResultSupreme.replicate(result, authData) } } } }
app/src/main/java/foundation/e/apps/data/login/playstore/UserProfileFetcher.kt 0 → 100644 +39 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.data.login.playstore import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.UserProfile import com.aurora.gplayapi.helpers.UserProfileHelper import foundation.e.apps.data.playstore.utils.GPlayHttpClient import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class UserProfileFetcher @Inject constructor( private val gPlayHttpClient: GPlayHttpClient ) { suspend fun fetch(authData: AuthData): UserProfile? = withContext(Dispatchers.IO) { runCatching { UserProfileHelper(authData).using(gPlayHttpClient).getUserProfile() }.getOrNull() } }
app/src/main/java/foundation/e/apps/data/playstore/PlayStoreRepository.kt +38 −5 Original line number Diff line number Diff line Loading @@ -159,7 +159,9 @@ class PlayStoreRepository @Inject constructor( // batch request response might not contain images and descriptions of the app suspend fun getAppsDetails(packageNames: List<String>): List<Application> = withContext(Dispatchers.IO) { var appDetails: List<GplayApp> = getAppDetailsHelper().getAppByPackageName(packageNames) var appDetails = retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageNames) } if (!isEmulator() && appDetails.all { it.versionCode == 0L } && isAnonymousUser()) { // Google Play returns limited result ( i.e. version code being 0) with a stale token, Loading @@ -168,7 +170,9 @@ class PlayStoreRepository @Inject constructor( refreshPlayStoreAuthentication() appDetails = getAppDetailsHelper().getAppByPackageName(packageNames) appDetails = retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageNames) } if (appDetails.all { it.versionCode == 0L }) { Timber.w("After refreshing auth, version code is still 0.") Loading @@ -181,8 +185,10 @@ class PlayStoreRepository @Inject constructor( override suspend fun getAppDetails(packageName: String): Application = withContext(Dispatchers.IO) { var appDetails: GplayApp = try { var appDetails = try { retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageName) } } catch (exception: GplayHttpRequestException) { if (exception.status == HttpURLConnection.HTTP_NOT_FOUND) { throw InternalException.AppNotFound() Loading @@ -197,7 +203,9 @@ class PlayStoreRepository @Inject constructor( refreshPlayStoreAuthentication() appDetails = getAppDetailsHelper().getAppByPackageName(packageName) appDetails = retryOnUnauthorized { getAppDetailsHelper().getAppByPackageName(packageName) } if (appDetails.versionCode == 0L) { Timber.w("After refreshing auth, version code is still 0. Giving up installation.") Loading @@ -220,6 +228,31 @@ class PlayStoreRepository @Inject constructor( authenticatorRepository.fetchAuthObjects(listOf(StoreType.PLAY_STORE)) } private suspend fun <T> retryOnUnauthorized(block: suspend () -> T): T { return try { block() } catch (exception: Exception) { val isUnauthorized: Boolean = when (exception) { is GplayHttpRequestException -> exception.status == HttpURLConnection.HTTP_UNAUTHORIZED is InternalException.AppNotFound -> gPlayHttpClient.responseCode.value == HttpURLConnection.HTTP_UNAUTHORIZED else -> false } if (!isUnauthorized) { throw exception } val refreshResult = authenticatorRepository.getValidatedAuthData() val isSuccess: Boolean = refreshResult.isSuccess() && refreshResult.data != null if (!isSuccess) { throw exception } block() } } suspend fun getAppDetailsWeb(packageName: String): Application? { val webAppDetailsHelper = WebAppDetailsHelper().using(gPlayHttpClient) Loading