From b4d89d0658749ef8d7339a52b7743f586de1e8d8 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Sat, 28 Sep 2024 10:59:17 +0100 Subject: [PATCH 1/2] Add support for fetching authTokens from microG Signed-off-by: Aayush Gupta --- app/src/main/AndroidManifest.xml | 4 ++ .../com/aurora/store/data/work/AuthWorker.kt | 62 ++++++++++++++-- .../java/com/aurora/store/util/CertUtil.kt | 5 ++ .../java/com/aurora/store/util/PackageUtil.kt | 12 ++++ .../store/view/ui/splash/SplashFragment.kt | 70 ++++++++++++++++++- 5 files changed, 146 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 98d0c8135..e4a34b0fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,10 @@ + + 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 b62cda05d..3ce970815 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 @@ -1,7 +1,13 @@ package com.aurora.store.data.work +import android.accounts.Account +import android.accounts.AccountManager import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Base64 import android.util.Log +import androidx.core.os.bundleOf import androidx.hilt.work.HiltWorker import androidx.work.CoroutineWorker import androidx.work.WorkerParameters @@ -10,8 +16,16 @@ import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.data.model.AccountType import com.aurora.store.data.providers.AccountProvider import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +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 /** * Worker to refresh [AuthData] in background @@ -26,6 +40,8 @@ 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!") @@ -42,14 +58,48 @@ open class AuthWorker @AssistedInject constructor( val accountType = AccountProvider.getAccountType(appContext) val authData = when (accountType) { AccountType.GOOGLE -> { - authProvider.buildGoogleAuthData( - AccountProvider.getLoginEmail(appContext)!!, - AccountProvider.getLoginToken(appContext)!!.first, - AccountProvider.getLoginToken(appContext)!!.second - ).getOrThrow() + val email = AccountProvider.getLoginEmail(appContext)!! + val token = AccountProvider.getLoginToken(appContext)!!.first + val tokenType = AccountProvider.getLoginToken(appContext)!!.second + + 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() + } + } } - AccountType.ANONYMOUS -> authProvider.buildAnonymousAuthData().getOrThrow() + AccountType.ANONYMOUS -> { + Log.i(TAG, "Refreshing AuthData for anonymous account") + authProvider.buildAnonymousAuthData().getOrThrow() + } } require(verifyAndSaveAuth(authData, accountType) != null) diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index aa8d35d9b..445dc2dbd 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -37,6 +37,11 @@ object CertUtil { private val TAG = "CertUtil" + const val GOOGLE_ACCOUNT_TYPE = "com.google" + const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay" + const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending" + const val GOOGLE_PLAY_CERT = "MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK" + fun isFDroidApp(context: Context, packageName: String): Boolean { return isInstalledByFDroid(context, packageName) || isSignedByFDroid(context, packageName) } diff --git a/app/src/main/java/com/aurora/store/util/PackageUtil.kt b/app/src/main/java/com/aurora/store/util/PackageUtil.kt index 540f06b23..40e8c618a 100644 --- a/app/src/main/java/com/aurora/store/util/PackageUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PackageUtil.kt @@ -46,6 +46,10 @@ object PackageUtil { private const val TAG = "PackageUtil" + private const val PACKAGE_NAME_MICRO_G = "com.google.android.gms" + private const val VERSION_CODE_MICRO_G = 240913402 + private const val VERSION_CODE_MICRO_G_HUAWEI = 240913007 + fun getAllValidPackages(context: Context): List { val sharedLibs = context.packageManager.systemSharedLibraryNames ?: emptyArray() return context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA) @@ -57,6 +61,14 @@ object PackageUtil { } } + fun hasSupportedMicroG(context: Context): Boolean { + return if (isHuawei) { + isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G_HUAWEI) + } else { + isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G) + } + } + fun isInstalled(context: Context, packageName: String): Boolean { return try { getPackageInfo(context, packageName, PackageManager.GET_META_DATA) 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 5ed9e279f..fbc7a0979 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 @@ -19,19 +19,34 @@ package com.aurora.store.view.ui.splash +import android.accounts.Account +import android.accounts.AccountManager import android.content.Intent import android.net.UrlQuerySanitizer import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.util.Base64 +import android.util.Log import android.view.View import androidx.core.view.isVisible +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.os.bundleOf import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.extensions.hide +import com.aurora.extensions.isMAndAbove import com.aurora.extensions.show +import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.R import com.aurora.store.data.model.AuthState import com.aurora.store.databinding.FragmentSplashBinding +import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT +import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME +import com.aurora.store.util.PackageUtil import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB import com.aurora.store.util.Preferences.PREFERENCE_INTRO @@ -44,8 +59,20 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class SplashFragment : BaseFragment() { + private val TAG = SplashFragment::class.java.simpleName + private val viewModel: AuthViewModel by activityViewModels() + private val startForAccount = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + val accountName = it.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) + if (!accountName.isNullOrBlank()) { + requestAuthTokenForGoogle(accountName) + } else { + findNavController().navigate(R.id.googleFragment) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -176,7 +203,22 @@ class SplashFragment : BaseFragment() { binding.btnGoogle.addOnClickListener { if (viewModel.authState.value != AuthState.Fetching) { binding.btnGoogle.updateProgress(true) - findNavController().navigate(R.id.googleFragment) + if (isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext())) { + Log.i(TAG, "Found supported microG, trying to request credentials") + val accountIntent = AccountManager.newChooseAccountIntent( + null, + null, + arrayOf(GOOGLE_ACCOUNT_TYPE), + null, + null, + null, + null + ) + startForAccount.launch(accountIntent) + } else { + findNavController().navigate(R.id.googleFragment) + } + } } } @@ -221,4 +263,30 @@ class SplashFragment : BaseFragment() { requireArguments().getString("packageName") ?: "" } } + + private fun requestAuthTokenForGoogle(accountName: String) { + try { + AccountManager.get(requireContext()) + .getAuthToken( + Account(accountName, GOOGLE_ACCOUNT_TYPE), + GOOGLE_PLAY_AUTH_TOKEN_TYPE, + bundleOf( + "overridePackage" to GOOGLE_PLAY_PACKAGE_NAME, + "overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT) + ), + requireActivity(), + { + viewModel.buildGoogleAuthData( + accountName, + it.result.getString(AccountManager.KEY_AUTHTOKEN)!!, + AuthHelper.Token.AUTH + ) + }, + Handler(Looper.getMainLooper()) + ) + } catch (exception: Exception) { + Log.e(TAG, "Failed to get authToken for Google login") + findNavController().navigate(R.id.googleFragment) + } + } } From baa954eb20ddd454814dc93f598dd6296535d484 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sat, 14 Dec 2024 13:13:33 +0530 Subject: [PATCH 2/2] Do not show account selection dialog, if there is only 1 account --- app/src/main/AndroidManifest.xml | 2 ++ .../store/view/ui/splash/SplashFragment.kt | 25 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e4a34b0fa..e0f150b58 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,8 @@ + + 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 fbc7a0979..0ed6598f1 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 @@ -29,14 +29,15 @@ import android.os.Looper import android.util.Base64 import android.util.Log import android.view.View -import androidx.core.view.isVisible import androidx.activity.result.contract.ActivityResultContracts import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.aurora.extensions.hide import com.aurora.extensions.isMAndAbove +import com.aurora.extensions.runOnUiThread import com.aurora.extensions.show import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.store.R @@ -69,7 +70,7 @@ class SplashFragment : BaseFragment() { if (!accountName.isNullOrBlank()) { requestAuthTokenForGoogle(accountName) } else { - findNavController().navigate(R.id.googleFragment) + runOnUiThread { resetActions() } } } @@ -204,6 +205,14 @@ class SplashFragment : BaseFragment() { if (viewModel.authState.value != AuthState.Fetching) { binding.btnGoogle.updateProgress(true) if (isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext())) { + val accounts = fetchGoogleAccounts() + + // Do not show selection dialog if there is only one account available + if (accounts.isNotEmpty() && accounts.size == 1) { + requestAuthTokenForGoogle(accounts.first().name) + return@addOnClickListener + } + Log.i(TAG, "Found supported microG, trying to request credentials") val accountIntent = AccountManager.newChooseAccountIntent( null, @@ -218,7 +227,6 @@ class SplashFragment : BaseFragment() { } else { findNavController().navigate(R.id.googleFragment) } - } } } @@ -236,7 +244,8 @@ class SplashFragment : BaseFragment() { } private fun navigateToDefaultTab() { - val defaultDestination = Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) + val defaultDestination = + Preferences.getInteger(requireContext(), PREFERENCE_DEFAULT_SELECTED_TAB) val directions = when (requireArguments().getInt("destinationId", defaultDestination)) { R.id.updatesFragment -> { @@ -264,6 +273,11 @@ class SplashFragment : BaseFragment() { } } + private fun fetchGoogleAccounts(): Array { + val accountManager = AccountManager.get(requireContext()) + return accountManager.getAccountsByType(GOOGLE_ACCOUNT_TYPE) + } + private fun requestAuthTokenForGoogle(accountName: String) { try { AccountManager.get(requireContext()) @@ -278,7 +292,7 @@ class SplashFragment : BaseFragment() { { viewModel.buildGoogleAuthData( accountName, - it.result.getString(AccountManager.KEY_AUTHTOKEN)!!, + it.result.getString(AccountManager.KEY_AUTHTOKEN) ?: "", AuthHelper.Token.AUTH ) }, @@ -286,7 +300,6 @@ class SplashFragment : BaseFragment() { ) } catch (exception: Exception) { Log.e(TAG, "Failed to get authToken for Google login") - findNavController().navigate(R.id.googleFragment) } } }