improve microG account login & token refresh logic
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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!!)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<String?> = 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<Bundle>
|
||||
) {
|
||||
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)
|
||||
|
||||
@@ -173,6 +173,10 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
|
||||
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<FragmentSplashBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user