From 585284fb103eb27a516da1a382e6d8af06f5fd59 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 2 Mar 2025 13:54:00 +0000 Subject: [PATCH] improve microG account login & token refresh logic --- .../java/com/aurora/store/data/model/State.kt | 1 + .../store/data/providers/AuthProvider.kt | 16 +-- .../com/aurora/store/data/work/AuthWorker.kt | 115 ++++++++++++------ .../store/view/ui/splash/SplashFragment.kt | 14 ++- .../store/viewmodel/auth/AuthViewModel.kt | 52 ++++---- 5 files changed, 121 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/aurora/store/data/model/State.kt b/app/src/main/java/com/aurora/store/data/model/State.kt index bca70557a..1fd9c0231 100644 --- a/app/src/main/java/com/aurora/store/data/model/State.kt +++ b/app/src/main/java/com/aurora/store/data/model/State.kt @@ -41,5 +41,6 @@ sealed class AuthState { data object Valid : AuthState() data object Fetching : AuthState() data object Verifying : AuthState() + data class PendingAccountManager(val email: String, val token: String) : AuthState() data class Failed(val status: String) : AuthState() } diff --git a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt index 890b5ad25..d7c78d405 100644 --- a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt @@ -72,20 +72,8 @@ class AuthProvider @Inject constructor( /** * Checks whether saved AuthData is valid or not */ - suspend fun isSavedAuthDataValid(): Boolean { - // TODO: Switch to the method from gplayapi - return withContext(Dispatchers.IO) { - try { - val testPackageName = "com.android.chrome" - val app = AppDetailsHelper(authData!!) - .using(httpClient) - .getAppByPackageName(testPackageName) - app.packageName == testPackageName && app.displayName.isNotBlank() - && app.versionCode != 0 - } catch (exception: Exception) { - false - } - } + fun isSavedAuthDataValid(): Boolean { + return AuthHelper.isValid(authData!!) } /** diff --git a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt index 3ce970815..56b18abfa 100644 --- a/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/AuthWorker.kt @@ -2,7 +2,9 @@ package com.aurora.store.data.work import android.accounts.Account import android.accounts.AccountManager +import android.accounts.AccountManagerCallback import android.content.Context +import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Base64 @@ -22,10 +24,9 @@ import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.runBlocking +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine /** * Worker to refresh [AuthData] in background @@ -40,8 +41,6 @@ open class AuthWorker @AssistedInject constructor( private val TAG = AuthWorker::class.java.simpleName - private val authToken: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) - override suspend fun doWork(): Result { if (!AccountProvider.isLoggedIn(appContext)) { Log.i(TAG, "User has logged out!") @@ -58,40 +57,23 @@ open class AuthWorker @AssistedInject constructor( val accountType = AccountProvider.getAccountType(appContext) val authData = when (accountType) { AccountType.GOOGLE -> { - val email = AccountProvider.getLoginEmail(appContext)!! - val token = AccountProvider.getLoginToken(appContext)!!.first - val tokenType = AccountProvider.getLoginToken(appContext)!!.second + val email = AccountProvider.getLoginEmail(appContext) + val tokenPair = AccountProvider.getLoginToken(appContext) - if (tokenType == AuthHelper.Token.AAS) { - Log.i(TAG, "Refreshing AuthData for personal account") - authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow() - } else { - /* - * We are working with AuthToken here. The only scenario when we will have - * AuthToken and Google login is when the user used microG to login into - * Aurora Store. In this case, we use system's AccountManager to request credentials. - */ - Log.i(TAG, "Refreshing AuthData for personal account using AccountManager") - AccountManager.get(appContext) - .getAuthToken( - Account(email, GOOGLE_ACCOUNT_TYPE), - GOOGLE_PLAY_AUTH_TOKEN_TYPE, - bundleOf( - "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, - "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) - ), - true, - { - authToken.tryEmit(it.result.getString(AccountManager.KEY_AUTHTOKEN)) - }, - Handler(Looper.getMainLooper()) - ) - runBlocking { - authProvider.buildGoogleAuthData( - email, - authToken.take(1).first()!!, - tokenType - ).getOrThrow() + if (email == null || tokenPair == null) { + throw Exception() + } + + when (tokenPair.second) { + AuthHelper.Token.AAS -> { + Log.i(TAG, "Refreshing AuthData for personal account") + authProvider.buildGoogleAuthData(email, tokenPair.first, AuthHelper.Token.AAS).getOrThrow() + } + + AuthHelper.Token.AUTH -> { + Log.i(TAG, "Refreshing AuthData for personal account using AccountManager") + val newToken = fetchAuthToken(email, tokenPair.first) + authProvider.buildGoogleAuthData(email, newToken, AuthHelper.Token.AAS).getOrThrow() } } } @@ -111,6 +93,61 @@ open class AuthWorker @AssistedInject constructor( } } + private suspend fun fetchAuthToken(email: String, oldToken: String? = null): String { + return suspendCoroutine { continuation -> + fetchAuthToken(email, oldToken) { future -> + try { + val bundle = future.result + val token = bundle.getString(AccountManager.KEY_AUTHTOKEN) + + if (token != null) { + continuation.resume(token) + } else { + continuation.resumeWithException(IllegalStateException("Auth token is null")) + } + } catch (e: Exception) { + continuation.resumeWithException(e) + } + } + } + } + + private fun fetchAuthToken( + email: String, + oldToken: String? = null, + callback: AccountManagerCallback + ) { + try { + if (oldToken != null) { + // Invalidate the old token before requesting a new one + AccountManager.get(appContext) + .invalidateAuthToken( + GOOGLE_ACCOUNT_TYPE, + oldToken + ) + } + + AccountManager.get(appContext) + .getAuthToken( + Account(email, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + bundleOf( + "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overrideCertificate" to Base64.decode( + GOOGLE_PLAY_CERT, + Base64.DEFAULT + ) + ), + true, + callback, + Handler(Looper.getMainLooper()) + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to fetch auth token", e) + callback.run(null) + } + } + private fun verifyAndSaveAuth(authData: AuthData, accountType: AccountType): AuthData? { return if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) { authProvider.saveAuthData(authData) diff --git a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt index 277c0f322..51e3f4b99 100644 --- a/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/splash/SplashFragment.kt @@ -173,6 +173,10 @@ class SplashFragment : BaseFragment() { updateStatus(getString(R.string.verifying_new_session)) } + is AuthState.PendingAccountManager -> { + requestAuthTokenForGoogle(it.email, it.token) + } + is AuthState.Failed -> { updateStatus(it.status) updateActionLayout(true) @@ -271,8 +275,16 @@ class SplashFragment : BaseFragment() { } } - private fun requestAuthTokenForGoogle(accountName: String) { + private fun requestAuthTokenForGoogle(accountName: String, oldToken: String? = null) { try { + if (oldToken != null) { + // Invalidate the old token before requesting a new one + AccountManager.get(requireContext()).invalidateAuthToken( + GOOGLE_ACCOUNT_TYPE, + oldToken + ) + } + AccountManager.get(requireContext()) .getAuthToken( Account(accountName, GOOGLE_ACCOUNT_TYPE), diff --git a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt index 1b952fcff..5db1cc74e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/auth/AuthViewModel.kt @@ -128,35 +128,41 @@ class AuthViewModel @Inject constructor( } } - private fun buildSavedAuthData() { - viewModelScope.launch(Dispatchers.IO) { - try { - if (authProvider.isSavedAuthDataValid()) { - _authState.value = AuthState.Valid - } else { - // Generate and validate new auth - when (AccountProvider.getAccountType(context)) { - AccountType.GOOGLE -> { - buildGoogleAuthData( - AccountProvider.getLoginEmail(context)!!, - AccountProvider.getLoginToken(context)!!.first, - AccountProvider.getLoginToken(context)!!.second - ) + private fun buildSavedAuthData() = viewModelScope.launch(Dispatchers.IO) { + try { + if (authProvider.isSavedAuthDataValid()) { + _authState.value = AuthState.Valid + } else { + // Generate and validate new auth + when (AccountProvider.getAccountType(context)) { + AccountType.ANONYMOUS -> buildAnonymousAuthData() + AccountType.GOOGLE -> { + val email = AccountProvider.getLoginEmail(context) + val tokenPair = AccountProvider.getLoginToken(context) + + if (email == null || tokenPair == null) { + throw Exception() } - AccountType.ANONYMOUS -> { - buildAnonymousAuthData() + when (tokenPair.second) { + AuthHelper.Token.AAS -> { + buildGoogleAuthData(email, tokenPair.first, AuthHelper.Token.AAS) + } + + AuthHelper.Token.AUTH -> { + _authState.value = AuthState.PendingAccountManager(email, tokenPair.first) + } } } } - } catch (exception: Exception) { - val error = when (exception) { - is UnknownHostException -> context.getString(R.string.title_no_network) - is ConnectException -> context.getString(R.string.server_unreachable) - else -> context.getString(R.string.bad_request) - } - _authState.value = AuthState.Failed(error) } + } catch (exception: Exception) { + val error = when (exception) { + is UnknownHostException -> context.getString(R.string.title_no_network) + is ConnectException -> context.getString(R.string.server_unreachable) + else -> context.getString(R.string.bad_request) + } + _authState.value = AuthState.Failed(error) } }