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 Valid : AuthState()
|
||||||
data object Fetching : AuthState()
|
data object Fetching : AuthState()
|
||||||
data object Verifying : AuthState()
|
data object Verifying : AuthState()
|
||||||
|
data class PendingAccountManager(val email: String, val token: String) : AuthState()
|
||||||
data class Failed(val status: 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
|
* Checks whether saved AuthData is valid or not
|
||||||
*/
|
*/
|
||||||
suspend fun isSavedAuthDataValid(): Boolean {
|
fun isSavedAuthDataValid(): Boolean {
|
||||||
// TODO: Switch to the method from gplayapi
|
return AuthHelper.isValid(authData!!)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.aurora.store.data.work
|
|||||||
|
|
||||||
import android.accounts.Account
|
import android.accounts.Account
|
||||||
import android.accounts.AccountManager
|
import android.accounts.AccountManager
|
||||||
|
import android.accounts.AccountManagerCallback
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
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 com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlin.coroutines.resume
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker to refresh [AuthData] in background
|
* Worker to refresh [AuthData] in background
|
||||||
@@ -40,8 +41,6 @@ open class AuthWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
private val TAG = AuthWorker::class.java.simpleName
|
private val TAG = AuthWorker::class.java.simpleName
|
||||||
|
|
||||||
private val authToken: MutableSharedFlow<String?> = MutableSharedFlow(extraBufferCapacity = 1)
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
if (!AccountProvider.isLoggedIn(appContext)) {
|
if (!AccountProvider.isLoggedIn(appContext)) {
|
||||||
Log.i(TAG, "User has logged out!")
|
Log.i(TAG, "User has logged out!")
|
||||||
@@ -58,40 +57,23 @@ open class AuthWorker @AssistedInject constructor(
|
|||||||
val accountType = AccountProvider.getAccountType(appContext)
|
val accountType = AccountProvider.getAccountType(appContext)
|
||||||
val authData = when (accountType) {
|
val authData = when (accountType) {
|
||||||
AccountType.GOOGLE -> {
|
AccountType.GOOGLE -> {
|
||||||
val email = AccountProvider.getLoginEmail(appContext)!!
|
val email = AccountProvider.getLoginEmail(appContext)
|
||||||
val token = AccountProvider.getLoginToken(appContext)!!.first
|
val tokenPair = AccountProvider.getLoginToken(appContext)
|
||||||
val tokenType = AccountProvider.getLoginToken(appContext)!!.second
|
|
||||||
|
|
||||||
if (tokenType == AuthHelper.Token.AAS) {
|
if (email == null || tokenPair == null) {
|
||||||
Log.i(TAG, "Refreshing AuthData for personal account")
|
throw Exception()
|
||||||
authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow()
|
}
|
||||||
} else {
|
|
||||||
/*
|
when (tokenPair.second) {
|
||||||
* We are working with AuthToken here. The only scenario when we will have
|
AuthHelper.Token.AAS -> {
|
||||||
* AuthToken and Google login is when the user used microG to login into
|
Log.i(TAG, "Refreshing AuthData for personal account")
|
||||||
* Aurora Store. In this case, we use system's AccountManager to request credentials.
|
authProvider.buildGoogleAuthData(email, tokenPair.first, AuthHelper.Token.AAS).getOrThrow()
|
||||||
*/
|
}
|
||||||
Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
|
|
||||||
AccountManager.get(appContext)
|
AuthHelper.Token.AUTH -> {
|
||||||
.getAuthToken(
|
Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
|
||||||
Account(email, GOOGLE_ACCOUNT_TYPE),
|
val newToken = fetchAuthToken(email, tokenPair.first)
|
||||||
GOOGLE_PLAY_AUTH_TOKEN_TYPE,
|
authProvider.buildGoogleAuthData(email, newToken, AuthHelper.Token.AAS).getOrThrow()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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? {
|
private fun verifyAndSaveAuth(authData: AuthData, accountType: AccountType): AuthData? {
|
||||||
return if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) {
|
return if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) {
|
||||||
authProvider.saveAuthData(authData)
|
authProvider.saveAuthData(authData)
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
|
|||||||
updateStatus(getString(R.string.verifying_new_session))
|
updateStatus(getString(R.string.verifying_new_session))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AuthState.PendingAccountManager -> {
|
||||||
|
requestAuthTokenForGoogle(it.email, it.token)
|
||||||
|
}
|
||||||
|
|
||||||
is AuthState.Failed -> {
|
is AuthState.Failed -> {
|
||||||
updateStatus(it.status)
|
updateStatus(it.status)
|
||||||
updateActionLayout(true)
|
updateActionLayout(true)
|
||||||
@@ -271,8 +275,16 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestAuthTokenForGoogle(accountName: String) {
|
private fun requestAuthTokenForGoogle(accountName: String, oldToken: String? = null) {
|
||||||
try {
|
try {
|
||||||
|
if (oldToken != null) {
|
||||||
|
// Invalidate the old token before requesting a new one
|
||||||
|
AccountManager.get(requireContext()).invalidateAuthToken(
|
||||||
|
GOOGLE_ACCOUNT_TYPE,
|
||||||
|
oldToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AccountManager.get(requireContext())
|
AccountManager.get(requireContext())
|
||||||
.getAuthToken(
|
.getAuthToken(
|
||||||
Account(accountName, GOOGLE_ACCOUNT_TYPE),
|
Account(accountName, GOOGLE_ACCOUNT_TYPE),
|
||||||
|
|||||||
@@ -128,35 +128,41 @@ class AuthViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildSavedAuthData() {
|
private fun buildSavedAuthData() = viewModelScope.launch(Dispatchers.IO) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
try {
|
||||||
try {
|
if (authProvider.isSavedAuthDataValid()) {
|
||||||
if (authProvider.isSavedAuthDataValid()) {
|
_authState.value = AuthState.Valid
|
||||||
_authState.value = AuthState.Valid
|
} else {
|
||||||
} else {
|
// Generate and validate new auth
|
||||||
// Generate and validate new auth
|
when (AccountProvider.getAccountType(context)) {
|
||||||
when (AccountProvider.getAccountType(context)) {
|
AccountType.ANONYMOUS -> buildAnonymousAuthData()
|
||||||
AccountType.GOOGLE -> {
|
AccountType.GOOGLE -> {
|
||||||
buildGoogleAuthData(
|
val email = AccountProvider.getLoginEmail(context)
|
||||||
AccountProvider.getLoginEmail(context)!!,
|
val tokenPair = AccountProvider.getLoginToken(context)
|
||||||
AccountProvider.getLoginToken(context)!!.first,
|
|
||||||
AccountProvider.getLoginToken(context)!!.second
|
if (email == null || tokenPair == null) {
|
||||||
)
|
throw Exception()
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountType.ANONYMOUS -> {
|
when (tokenPair.second) {
|
||||||
buildAnonymousAuthData()
|
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