From cb3ec1bbf520f9a1da02641bfc008d7909d495b2 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 5 Jun 2024 13:03:35 +0300 Subject: [PATCH 01/24] wip --- FlowCrypt/build.gradle.kts | 3 + .../email/api/email/FoldersManager.kt | 3 + .../email/database/entity/AccountEntity.kt | 19 +-- .../email/ui/activity/MainActivity.kt | 28 +++- .../activity/fragment/MainSignInFragment.kt | 120 ++++++++++++------ 5 files changed, 118 insertions(+), 55 deletions(-) diff --git a/FlowCrypt/build.gradle.kts b/FlowCrypt/build.gradle.kts index 6b2381f553..380a70981b 100644 --- a/FlowCrypt/build.gradle.kts +++ b/FlowCrypt/build.gradle.kts @@ -445,11 +445,14 @@ dependencies { implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.navigation:navigation-runtime-ktx:2.7.7") implementation("androidx.webkit:webkit:1.11.0") + implementation("androidx.credentials:credentials:1.2.2") + implementation("androidx.credentials:credentials-play-services-auth:1.2.2") implementation("com.google.android.gms:play-services-base:18.5.0") implementation("com.google.android.gms:play-services-auth:21.2.0") implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.flexbox:flexbox:3.0.0") + implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0") //https://mvnrepository.com/artifact/com.google.code.gson/gson implementation("com.google.code.gson:gson:2.11.0") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FoldersManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FoldersManager.kt index 59e38144ad..545cae32ef 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FoldersManager.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/FoldersManager.kt @@ -231,6 +231,9 @@ class FoldersManager constructor(val accountEntity: AccountEntity) { */ fun getSortedServerFolders(): Collection { val localFolders = serverFolders.toMutableList() + if (localFolders.size <= 1) { + return localFolders + } val sortedList = arrayOfNulls(localFolders.size) val inbox = folderInbox?.let { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index e7cc4af3a9..a6241f86ca 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -20,7 +20,7 @@ import com.flowcrypt.email.api.email.model.AuthCredentials import com.flowcrypt.email.api.email.model.SecurityType import com.flowcrypt.email.api.retrofit.response.model.ClientConfiguration import com.flowcrypt.email.security.KeyStoreCryptoManager -import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.parcelize.IgnoredOnParcel @@ -101,20 +101,20 @@ data class AccountEntity( get() = JavaEmailConstants.AUTH_MECHANISMS_XOAUTH2 == imapAuthMechanisms constructor( - googleSignInAccount: GoogleSignInAccount, + googleIdTokenCredential: GoogleIdTokenCredential, clientConfiguration: ClientConfiguration? = null, useCustomerFesUrl: Boolean, useStartTlsForSmtp: Boolean = false, ) : this( - email = requireNotNull(googleSignInAccount.email).lowercase(), - accountType = googleSignInAccount.account?.type?.lowercase(), - displayName = googleSignInAccount.displayName, - givenName = googleSignInAccount.givenName, - familyName = googleSignInAccount.familyName, - photoUrl = googleSignInAccount.photoUrl?.toString(), + email = requireNotNull(googleIdTokenCredential.id).lowercase(), + accountType = ACCOUNT_TYPE_GOOGLE, + displayName = googleIdTokenCredential.displayName, + givenName = googleIdTokenCredential.givenName, + familyName = googleIdTokenCredential.familyName, + photoUrl = googleIdTokenCredential.profilePictureUri?.toString(), isEnabled = true, isActive = false, - username = requireNotNull(googleSignInAccount.email), + username = requireNotNull(googleIdTokenCredential.id), password = "", imapServer = GmailConstants.GMAIL_IMAP_SERVER, imapPort = GmailConstants.GMAIL_IMAP_PORT, @@ -346,6 +346,7 @@ data class AccountEntity( companion object { const val TABLE_NAME = "accounts" const val ACCOUNT_TYPE_GOOGLE = "com.google" + const val ACCOUNT_TYPE_GOOGLE_SIGN_NEW = "com.google" const val ACCOUNT_TYPE_OUTLOOK = "outlook.com" } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt index 435cb18bbb..5271edb47c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt @@ -30,6 +30,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.GravityCompat +import androidx.credentials.ClearCredentialStateRequest +import androidx.credentials.CredentialManager +import androidx.credentials.exceptions.ClearCredentialException import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver @@ -61,6 +64,7 @@ import com.flowcrypt.email.extensions.android.content.getParcelableExtraViaExt import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.exceptionMsg import com.flowcrypt.email.extensions.incrementSafely +import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.extensions.kotlin.parseAsColorBasedOnDefaultSettings import com.flowcrypt.email.extensions.showFeedbackFragment import com.flowcrypt.email.extensions.showInfoDialog @@ -81,16 +85,13 @@ import com.flowcrypt.email.util.FlavorSettings import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.exception.CommonConnectionException import com.flowcrypt.email.util.exception.EmptyPassphraseException -import com.flowcrypt.email.util.google.GoogleApiClientHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInClient import kotlinx.coroutines.launch +import java.util.UUID /** * @author Denys Bondarenko */ class MainActivity : BaseActivity() { - private lateinit var client: GoogleSignInClient private var navigationViewManager: NavigationViewManager? = null private val launcherViewModel: LauncherViewModel by viewModels() @@ -155,8 +156,6 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) observeMovingToBackground() - client = GoogleSignIn.getClient(this, GoogleApiClientHelper.generateGoogleSignInOptions()) - IdleService.start(this) IdleService.bind(this, idleServiceConnection) @@ -393,7 +392,22 @@ class MainActivity : BaseActivity() { private fun logout() { lifecycleScope.launch { activeAccount?.let { accountEntity -> - if (accountEntity.accountType == AccountEntity.ACCOUNT_TYPE_GOOGLE) client.signOut() + if (accountEntity.accountType == AccountEntity.ACCOUNT_TYPE_GOOGLE) { + try { + CredentialManager.create(this@MainActivity).clearCredentialState( + ClearCredentialStateRequest() + ) + } catch (e: ClearCredentialException) { + //need to test bad connection. Maybe it will be better to use dialog here. + e.printStackTraceIfDebugOnly() + showInfoDialog( + requestKey = UUID.randomUUID().toString(), + dialogMsg = e.errorMessage?.toString(), + dialogTitle = getString(R.string.error) + ) + return@launch + } + } FlavorSettings.getCountingIdlingResource().incrementSafely(this@MainActivity) WorkManager.getInstance(applicationContext).cancelAllWorkByTag(BaseSyncWorker.TAG_SYNC) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 3ee79dbcbf..44e5cb55a0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -6,17 +6,24 @@ package com.flowcrypt.email.ui.activity.fragment import android.app.Activity -import android.content.Context import android.content.Intent +import android.content.IntentSender.SendIntentException import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import com.flowcrypt.email.BuildConfig import com.flowcrypt.email.Constants @@ -33,24 +40,26 @@ import com.flowcrypt.email.databinding.FragmentMainSignInBinding import com.flowcrypt.email.extensions.android.os.getParcelableArrayListViaExt import com.flowcrypt.email.extensions.android.os.getParcelableViaExt import com.flowcrypt.email.extensions.android.os.getSerializableViaExt -import com.flowcrypt.email.extensions.androidx.navigation.navigateSafe import com.flowcrypt.email.extensions.androidx.fragment.app.countingIdlingResource -import com.flowcrypt.email.extensions.decrementSafely -import com.flowcrypt.email.extensions.exceptionMsg import com.flowcrypt.email.extensions.androidx.fragment.app.getNavigationResult -import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.setFragmentResultListenerForTwoWayDialog import com.flowcrypt.email.extensions.androidx.fragment.app.showFeedbackFragment import com.flowcrypt.email.extensions.androidx.fragment.app.showInfoDialog import com.flowcrypt.email.extensions.androidx.fragment.app.showTwoWayDialog import com.flowcrypt.email.extensions.androidx.fragment.app.toast +import com.flowcrypt.email.extensions.androidx.navigation.navigateSafe +import com.flowcrypt.email.extensions.decrementSafely +import com.flowcrypt.email.extensions.exceptionMsg +import com.flowcrypt.email.extensions.incrementSafely +import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.jetpack.viewmodel.CheckCustomerUrlFesServerViewModel import com.flowcrypt.email.jetpack.viewmodel.ClientConfigurationViewModel import com.flowcrypt.email.jetpack.viewmodel.EkmViewModel import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.model.PgpKeyRingDetails import com.flowcrypt.email.service.CheckClipboardToFindKeyService +import com.flowcrypt.email.service.CheckClipboardToFindKeyService.Companion.TAG import com.flowcrypt.email.ui.activity.fragment.CheckKeysFragment.CheckingState.Companion.CHECKED_KEYS import com.flowcrypt.email.ui.activity.fragment.CheckKeysFragment.CheckingState.Companion.SKIP_REMAINING_KEYS import com.flowcrypt.email.ui.activity.fragment.base.BaseSingInFragment @@ -62,18 +71,22 @@ import com.flowcrypt.email.util.exception.EkmNotSupportedException import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.UnsupportedClientConfigurationException import com.flowcrypt.email.util.google.GoogleApiClientHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount -import com.google.android.gms.auth.api.signin.GoogleSignInClient -import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.tasks.Task +import com.google.android.gms.auth.api.identity.AuthorizationRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.common.api.Scope +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.android.material.snackbar.Snackbar import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import com.sun.mail.util.MailConnectException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.pgpainless.util.Passphrase import java.net.HttpURLConnection import java.net.SocketTimeoutException +import java.util.UUID import javax.net.ssl.SSLException /** @@ -83,8 +96,7 @@ class MainSignInFragment : BaseSingInFragment() { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentMainSignInBinding.inflate(inflater, container, false) - private lateinit var client: GoogleSignInClient - private var cachedGoogleSignInAccount: GoogleSignInAccount? = null + private var cachedGoogleIdTokenCredential: GoogleIdTokenCredential? = null private var cachedClientConfiguration: ClientConfiguration? = null private var cachedBaseFesUrlPath: String? = null @@ -116,11 +128,6 @@ class MainSignInFragment : BaseSingInFragment() { override val isDisplayHomeAsUpEnabled: Boolean get() = false - override fun onAttach(context: Context) { - super.onAttach(context) - client = GoogleSignIn.getClient(context, GoogleApiClientHelper.generateGoogleSignInOptions()) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initViews(view) @@ -139,9 +146,9 @@ class MainSignInFragment : BaseSingInFragment() { override fun getTempAccount(): AccountEntity? { val sharedTenantFesBaseUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false) - return cachedGoogleSignInAccount?.let { + return cachedGoogleIdTokenCredential?.let { AccountEntity( - googleSignInAccount = it, + googleIdTokenCredential = it, clientConfiguration = cachedClientConfiguration, useCustomerFesUrl = cachedBaseFesUrlPath?.isNotEmpty() == true && cachedBaseFesUrlPath != sharedTenantFesBaseUrlPath, @@ -212,9 +219,40 @@ class MainSignInFragment : BaseSingInFragment() { } private fun signInWithGmail() { - cachedGoogleSignInAccount = null - client.signOut() - forActivityResultSignIn.launch(client.signInIntent) + cachedGoogleIdTokenCredential = null + + val getSignInWithGoogleOption = + GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) + //need to think about nonce more + .setNonce(UUID.randomUUID().toString()) + .build() + + val getCredentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(getSignInWithGoogleOption) + .build() + + //need to test it with slow internet. Maybe need to use a dialog here + lifecycleScope.launch { + try { + val getCredentialResponse = CredentialManager.create(requireContext()).getCredential( + context = requireContext(), + request = getCredentialRequest + ) + withContext(Dispatchers.Main) { + handleSignIn(getCredentialResponse) + } + } catch (e: Exception) { + e.printStackTraceIfDebugOnly() + //need to test it + withContext(Dispatchers.Main) { + showInfoDialog( + dialogTitle = "", + dialogMsg = e.message ?: getString(R.string.unknown_error), + isCancelable = true + ) + } + } + } } private fun handleSignInResult(resultCode: Int, task: Task) { @@ -283,9 +321,9 @@ class MainSignInFragment : BaseSingInFragment() { } } - private fun onSignSuccess(googleSignInAccount: GoogleSignInAccount?) { + private fun onSignSuccess(googleIdTokenCredential: GoogleIdTokenCredential?) { val existedAccount = existingAccounts.firstOrNull { - it.email.equals(googleSignInAccount?.email, ignoreCase = true) + it.email.equals(googleIdTokenCredential?.id, ignoreCase = true) } if (existedAccount == null) { @@ -375,7 +413,7 @@ class MainSignInFragment : BaseSingInFragment() { if (original is MailConnectException && !useStartTlsForSmtp) { useStartTlsForSmtp = true - onSignSuccess(cachedGoogleSignInAccount) + onSignSuccess(cachedGoogleIdTokenCredential) return } @@ -439,15 +477,16 @@ class MainSignInFragment : BaseSingInFragment() { when (requestCode) { REQUEST_CODE_RETRY_CHECK_FES_AVAILABILITY -> if (result == TwoWayDialogFragment.RESULT_OK) { - val account = cachedGoogleSignInAccount?.account?.name + val account = cachedGoogleIdTokenCredential?.id ?: return@setFragmentResultListenerForTwoWayDialog checkCustomerUrlFesServerViewModel.checkServerAvailability(account) } REQUEST_CODE_RETRY_GET_CLIENT_CONFIGURATION -> if (result == TwoWayDialogFragment.RESULT_OK) { val idToken = - cachedGoogleSignInAccount?.idToken ?: return@setFragmentResultListenerForTwoWayDialog - val account = cachedGoogleSignInAccount?.account?.name + cachedGoogleIdTokenCredential?.idToken + ?: return@setFragmentResultListenerForTwoWayDialog + val account = cachedGoogleIdTokenCredential?.id ?: return@setFragmentResultListenerForTwoWayDialog val domain = EmailUtil.getDomain(account) val baseFesUrlPath = @@ -461,7 +500,8 @@ class MainSignInFragment : BaseSingInFragment() { REQUEST_CODE_RETRY_FETCH_PRV_KEYS_VIA_EKM -> if (result == TwoWayDialogFragment.RESULT_OK) { val idToken = - cachedGoogleSignInAccount?.idToken ?: return@setFragmentResultListenerForTwoWayDialog + cachedGoogleIdTokenCredential?.idToken + ?: return@setFragmentResultListenerForTwoWayDialog cachedClientConfiguration?.let { ekmViewModel.fetchPrvKeys(it, idToken) } } } @@ -496,13 +536,13 @@ class MainSignInFragment : BaseSingInFragment() { privateKeysViewModel.doAdditionalActionsAfterPrivateKeyCreation( accountEntity = account, keys = keys, - idToken = cachedGoogleSignInAccount?.idToken + idToken = cachedGoogleIdTokenCredential?.idToken ) } } CreateOrImportPrivateKeyDuringSetupFragment.Result.USE_ANOTHER_ACCOUNT -> { - this.cachedGoogleSignInAccount = null + this.cachedGoogleIdTokenCredential = null showContent() } } @@ -568,9 +608,9 @@ class MainSignInFragment : BaseSingInFragment() { Result.Status.SUCCESS -> { if (it.data?.service in ApiClientRepository.FES.ALLOWED_SERVICES) { - cachedGoogleSignInAccount?.account?.name?.let { account -> + cachedGoogleIdTokenCredential?.id?.let { account -> val domain = EmailUtil.getDomain(account) - val idToken = cachedGoogleSignInAccount?.idToken ?: return@let + val idToken = cachedGoogleIdTokenCredential?.idToken ?: return@let val baseFesUrlPath = GeneralUtil.genBaseFesUrlPath( useCustomerFesUrl = true, domain = domain @@ -634,6 +674,7 @@ class MainSignInFragment : BaseSingInFragment() { } is SSLException -> { + @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { showDialogWithRetryButton(it, REQUEST_CODE_RETRY_CHECK_FES_AVAILABILITY) } else { @@ -657,6 +698,7 @@ class MainSignInFragment : BaseSingInFragment() { } private fun continueBasedOnFlavorSettings(errorMsg: String) { + @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { showDialogWithRetryButton( errorMsg, @@ -668,8 +710,8 @@ class MainSignInFragment : BaseSingInFragment() { } private fun continueWithRegularFlow() { - val idToken = cachedGoogleSignInAccount?.idToken - val account = cachedGoogleSignInAccount?.account?.name + val idToken = cachedGoogleIdTokenCredential?.idToken + val account = cachedGoogleIdTokenCredential?.id val baseFesUrlPath = cachedBaseFesUrlPath if (idToken != null && account != null && baseFesUrlPath != null) { @@ -694,7 +736,7 @@ class MainSignInFragment : BaseSingInFragment() { } Result.Status.SUCCESS -> { - val idToken = cachedGoogleSignInAccount?.idToken + val idToken = cachedGoogleIdTokenCredential?.idToken cachedClientConfiguration = it.data?.clientConfiguration if (idToken != null) { @@ -752,7 +794,7 @@ class MainSignInFragment : BaseSingInFragment() { showContent() when (it.exception) { is EkmNotSupportedException -> { - onSignSuccess(cachedGoogleSignInAccount) + onSignSuccess(cachedGoogleIdTokenCredential) } is UnsupportedClientConfigurationException -> { @@ -848,7 +890,7 @@ class MainSignInFragment : BaseSingInFragment() { if (accountEntity == null) { showContent() - ExceptionUtil.handleError(NullPointerException("GoogleSignInAccount is null!")) + ExceptionUtil.handleError(NullPointerException("GoogleIdTokenCredential is null!")) toast(R.string.error_occurred_try_again_later) } else { accountViewModel.addNewAccount(accountEntity) From 8f1d476116a22ee4ae9625367b5bc6494f94e5f9 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 5 Jun 2024 20:02:33 +0300 Subject: [PATCH 02/24] wip --- FlowCrypt/build.gradle.kts | 2 +- .../activity/fragment/MainSignInFragment.kt | 197 ++++++++++++------ 2 files changed, 135 insertions(+), 64 deletions(-) diff --git a/FlowCrypt/build.gradle.kts b/FlowCrypt/build.gradle.kts index 380a70981b..ab86e10434 100644 --- a/FlowCrypt/build.gradle.kts +++ b/FlowCrypt/build.gradle.kts @@ -449,7 +449,7 @@ dependencies { implementation("androidx.credentials:credentials-play-services-auth:1.2.2") implementation("com.google.android.gms:play-services-base:18.5.0") - implementation("com.google.android.gms:play-services-auth:21.2.0") + implementation("com.google.android.gms:play-services-auth:21.2.0")//should be removed implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 44e5cb55a0..0bf9a4b748 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -7,7 +7,6 @@ package com.flowcrypt.email.ui.activity.fragment import android.app.Activity import android.content.Intent -import android.content.IntentSender.SendIntentException import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -106,16 +105,17 @@ class MainSignInFragment : BaseSingInFragment() { private var useStartTlsForSmtp = false private val forActivityResultSignIn = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() + ActivityResultContracts.StartIntentSenderForResult() ) { result: ActivityResult -> - handleSignInResult(result.resultCode, GoogleSignIn.getSignedInAccountFromIntent(result.data)) + toast("autorized!") + //handleSignIn(result.resultCode) } private val forActivityResultSignInError = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK) { - signInWithGmail() + signInWithGoogle() } } @@ -183,7 +183,7 @@ class MainSignInFragment : BaseSingInFragment() { cachedBaseFesUrlPath = null cachedClientConfiguration = null importCandidates.clear() - signInWithGmail() + signInWithGoogle() } view.findViewById(R.id.buttonOtherEmailProvider)?.setOnClickListener { @@ -218,7 +218,7 @@ class MainSignInFragment : BaseSingInFragment() { } } - private fun signInWithGmail() { + private fun signInWithGoogle() { cachedGoogleIdTokenCredential = null val getSignInWithGoogleOption = @@ -239,7 +239,7 @@ class MainSignInFragment : BaseSingInFragment() { request = getCredentialRequest ) withContext(Dispatchers.Main) { - handleSignIn(getCredentialResponse) + handleAuthentication(getCredentialResponse) } } catch (e: Exception) { e.printStackTraceIfDebugOnly() @@ -255,68 +255,139 @@ class MainSignInFragment : BaseSingInFragment() { } } - private fun handleSignInResult(resultCode: Int, task: Task) { - try { - if (task.isSuccessful) { - cachedGoogleSignInAccount = task.getResult(ApiException::class.java) - - val account = cachedGoogleSignInAccount?.account?.name ?: return - cachedBaseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false) - - val publicEmailDomains = EmailUtil.getPublicEmailDomains() - val domain = EmailUtil.getDomain(account) - if (domain in publicEmailDomains) { - if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { - cachedGoogleSignInAccount = null - showInfoDialog( - dialogTitle = "", - dialogMsg = getString( - R.string.enterprise_does_not_support_pub_domains, - getString(R.string.app_name), - domain - ), - isCancelable = true - ) - } else { - val idToken = cachedGoogleSignInAccount?.idToken - if (idToken == null) { - showInfoDialog( - dialogTitle = "", - dialogMsg = getString( - R.string.error_occurred_with_details_please_try_again, - "GoogleSignInAccount.idToken == null" - ), - isCancelable = true - ) + private fun handleAuthentication(getCredentialResponse: GetCredentialResponse) { + when (val credential = getCredentialResponse.credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + cachedGoogleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + val account = cachedGoogleIdTokenCredential?.id ?: return + cachedBaseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false) + + val publicEmailDomains = EmailUtil.getPublicEmailDomains() + val domain = EmailUtil.getDomain(account) + if (domain in publicEmailDomains) { + @Suppress("KotlinConstantConditions") + if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { + cachedGoogleIdTokenCredential = null + showInfoDialog( + dialogTitle = "", + dialogMsg = getString( + R.string.enterprise_does_not_support_pub_domains, + getString(R.string.app_name), + domain + ), + isCancelable = true + ) + } else { + val idToken = cachedGoogleIdTokenCredential?.idToken + if (idToken == null) { + showInfoDialog( + dialogTitle = "", + dialogMsg = getString( + R.string.error_occurred_with_details_please_try_again, + "GoogleIdTokenCredential.idToken == null" + ), + isCancelable = true + ) + } else { + val authorizationRequest: AuthorizationRequest = AuthorizationRequest.builder() + .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))).build() + Identity.getAuthorizationClient(requireContext()) + .authorize(authorizationRequest) + .addOnSuccessListener { authorizationResult -> + if (authorizationResult.hasResolution()) { + // Access needs to be granted by the user. + // At this stage the grant access to Gmail API screen should be displayed + val pendingIntent = authorizationResult.pendingIntent + + if (pendingIntent != null) { + try { + forActivityResultSignIn.launch( + IntentSenderRequest.Builder(pendingIntent.intentSender).build() + ) + } catch (e: Exception) { + //need to handle this case + Log.e(TAG, "Couldn't start Authorization UI: " + e.localizedMessage) + } + } else { + //need to handle this case + } + } else { + val oldToken = idToken + val newToken = authorizationResult.toGoogleSignInAccount()?.idToken + val accesToken = authorizationResult.accessToken + + println(oldToken + newToken + accesToken) + // Access already granted, continue with user action + clientConfigurationViewModel.fetchClientConfiguration( + idToken = accesToken!!, + baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), + domain = domain + ) + } + }.addOnFailureListener { exception -> + toast("addOnFailureListener") + Log.e( + TAG, + "Failed to authorize", + exception + ) + } + + /*clientConfigurationViewModel.fetchClientConfiguration( + idToken = idToken, + baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), + domain = domain + )*/ + } + } } else { - clientConfigurationViewModel.fetchClientConfiguration( - idToken = idToken, - baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), - domain = domain - ) + val authorizationRequest: AuthorizationRequest = AuthorizationRequest.builder() + .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))).build() + Identity.getAuthorizationClient(requireContext()) + .authorize(authorizationRequest) + .addOnSuccessListener { authorizationResult -> + if (authorizationResult.hasResolution()) { + // Access needs to be granted by the user. + // At this stage the grant access to Gmail API screen should be displayed + val pendingIntent = authorizationResult.pendingIntent + + if (pendingIntent != null) { + try { + forActivityResultSignIn.launch( + IntentSenderRequest.Builder(pendingIntent.intentSender).build() + ) + } catch (e: Exception) { + //need to handle this case + Log.e(TAG, "Couldn't start Authorization UI: " + e.localizedMessage) + } + } else { + //need to handle this case + } + } else { + // Access already granted, continue with user action + checkCustomerUrlFesServerViewModel.checkServerAvailability(account) + } + }.addOnFailureListener { exception -> + toast("addOnFailureListener") + Log.e( + TAG, + "Failed to authorize", + exception + ) + } } + } catch (e: GoogleIdTokenParsingException) { + Log.e(TAG, "Received an invalid google id token response", e) } } else { - checkCustomerUrlFesServerViewModel.checkServerAvailability(account) + toast("Unsupported credentials") } - } else { - val error = task.exception - - if (error is ApiException) { - throw error - } - - showInfoSnackbar( - msgText = error?.message ?: error?.javaClass?.simpleName - ?: getString(R.string.unknown_error) - ) } - } catch (e: ApiException) { - val msg = GoogleSignInStatusCodes.getStatusCodeString(e.statusCode) - if (resultCode == Activity.RESULT_OK) { - showInfoSnackbar(msgText = msg) - } else { - toast(msg) + + else -> { + toast("Unsupported credentials") } } } From cc31ff3d11ff26a5044e6d09e5084d9091f2358b Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 5 Jun 2024 20:58:20 +0300 Subject: [PATCH 03/24] wip --- .../viewmodel/SignInWithGoogleViewModel.kt | 94 +++++++++++++++++++ .../activity/fragment/MainSignInFragment.kt | 7 +- FlowCrypt/src/main/res/values-ru/strings.xml | 1 + FlowCrypt/src/main/res/values-uk/strings.xml | 1 + FlowCrypt/src/main/res/values/strings.xml | 1 + 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt new file mode 100644 index 0000000000..3f96bf6ab1 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -0,0 +1,94 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.jetpack.viewmodel + +import android.app.Application +import android.content.Context +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.lifecycle.viewModelScope +import com.flowcrypt.email.R +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.util.coroutines.runners.ControlledRunner +import com.flowcrypt.email.util.google.GoogleApiClientHelper +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.jose4j.jwt.consumer.JwtConsumerBuilder +import java.util.UUID + +/** + * @author Denys Bondarenko + */ +class SignInWithGoogleViewModel(application: Application) : AccountViewModel(application) { + private val controlledRunnerForGoogleIdTokenCredential = + ControlledRunner>() + private val googleIdTokenCredentialMutableStateFlow: MutableStateFlow> = + MutableStateFlow(Result.none()) + val googleIdTokenCredentialStateFlow: StateFlow> = + googleIdTokenCredentialMutableStateFlow.asStateFlow() + + fun authenticateUser() { + viewModelScope.launch { + googleIdTokenCredentialMutableStateFlow.value = Result.loading() + googleIdTokenCredentialMutableStateFlow.value = + controlledRunnerForGoogleIdTokenCredential.cancelPreviousThenRun { + val context: Context = getApplication() + + val nonce = UUID.randomUUID().toString() + val getSignInWithGoogleOption = + GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) + .setNonce(nonce) + .build() + + val getCredentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(getSignInWithGoogleOption) + .build() + + try { + val getCredentialResponse = CredentialManager.create(context).getCredential( + context = context, + request = getCredentialRequest + ) + + when (val credential = getCredentialResponse.credential) { + is CustomCredential -> { + if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + + //compare nonce from JWT + //mode details cab be found here https://auth0.com/docs/get-started/authentication-and-authorization-flow/implicit-flow-with-form-post/mitigate-replay-attacks-when-using-the-implicit-flow + val idToken = googleIdTokenCredential.idToken + val jwtConsumerBuilder = JwtConsumerBuilder() + .setSkipSignatureVerification() + .setExpectedAudience(GoogleApiClientHelper.SERVER_CLIENT_ID) + .build() + val claims = jwtConsumerBuilder.processToClaims(idToken) + if (claims.getClaimValueAsString("nonce") != nonce) { + throw IllegalStateException("Security error: nonce mismatch") + } + + return@cancelPreviousThenRun Result.success(googleIdTokenCredential) + } else { + throw IllegalStateException(context.getString(R.string.unsupported_credentials)) + } + } + + else -> { + throw IllegalStateException(context.getString(R.string.unsupported_credentials)) + } + } + } catch (e: Exception) { + Result.exception(e) + } + } + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 0bf9a4b748..85527782c6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -55,6 +55,7 @@ import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.jetpack.viewmodel.CheckCustomerUrlFesServerViewModel import com.flowcrypt.email.jetpack.viewmodel.ClientConfigurationViewModel import com.flowcrypt.email.jetpack.viewmodel.EkmViewModel +import com.flowcrypt.email.jetpack.viewmodel.SignInWithGoogleViewModel import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.model.PgpKeyRingDetails import com.flowcrypt.email.service.CheckClipboardToFindKeyService @@ -102,6 +103,7 @@ class MainSignInFragment : BaseSingInFragment() { private val checkCustomerUrlFesServerViewModel: CheckCustomerUrlFesServerViewModel by viewModels() private val clientConfigurationViewModel: ClientConfigurationViewModel by viewModels() private val ekmViewModel: EkmViewModel by viewModels() + private val signInWithGoogleViewModel: SignInWithGoogleViewModel by viewModels() private var useStartTlsForSmtp = false private val forActivityResultSignIn = registerForActivityResult( @@ -219,6 +221,9 @@ class MainSignInFragment : BaseSingInFragment() { } private fun signInWithGoogle() { + signInWithGoogleViewModel.authenticateUser() + return + cachedGoogleIdTokenCredential = null val getSignInWithGoogleOption = @@ -231,7 +236,7 @@ class MainSignInFragment : BaseSingInFragment() { .addCredentialOption(getSignInWithGoogleOption) .build() - //need to test it with slow internet. Maybe need to use a dialog here + //need to test it with slow internet. Maybe need to use a dialog here lifecycleScope.launch { try { val getCredentialResponse = CredentialManager.create(requireContext()).getCredential( diff --git a/FlowCrypt/src/main/res/values-ru/strings.xml b/FlowCrypt/src/main/res/values-ru/strings.xml index 1bc10c3b09..18c238d867 100644 --- a/FlowCrypt/src/main/res/values-ru/strings.xml +++ b/FlowCrypt/src/main/res/values-ru/strings.xml @@ -618,6 +618,7 @@ Этот исполняемый файл не был проверен на наличие вирусов, и его загрузка или запуск могут быть опасны.\n\nВсе равно продолжить? Исходящие: отправка… Сообщение не найдено или ярлыки изменены + Неподдерживаемые учетные данные Для Вашей защиты и безопасности данных в настоящее время осталось только %1$d попытка, после чего следует 5-минутный период восстановления. Для Вашей защиты и безопасности данных в настоящее время осталось только %1$d попытки, после чего следует 5-минутный период восстановления. diff --git a/FlowCrypt/src/main/res/values-uk/strings.xml b/FlowCrypt/src/main/res/values-uk/strings.xml index 92906815cc..8b56a5ae7c 100644 --- a/FlowCrypt/src/main/res/values-uk/strings.xml +++ b/FlowCrypt/src/main/res/values-uk/strings.xml @@ -619,6 +619,7 @@ Цей виконуваний файл не було перевірено на наявність вірусів, він може бути небезпечним для завантаження чи запуску.\n\nБажаєте продовжити? Вихідні: надсилання… Повідомлення не знайдено або мітки змінено + Непідтримувані облікові дані Для Вашого захисту та безпеки даних наразі залишилася лише %1$d спроба, а потім 5-хвилинний період відновлення. Для Вашого захисту та безпеки даних наразі залишилася лише %1$d спроби, а потім 5-хвилинний період відновлення. diff --git a/FlowCrypt/src/main/res/values/strings.xml b/FlowCrypt/src/main/res/values/strings.xml index 6df81a976b..3305843a01 100644 --- a/FlowCrypt/src/main/res/values/strings.xml +++ b/FlowCrypt/src/main/res/values/strings.xml @@ -640,4 +640,5 @@ This executable file was not checked for viruses, and may be dangerous to download or run.\n\nProceed anyway? Outbox: sending… Message not found or labels changed + Unsupported credentials From 0d1507080f8852fc49dab42faa11107839266ac6 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 10:05:40 +0300 Subject: [PATCH 04/24] wip --- .../java/com/flowcrypt/email/database/entity/AccountEntity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index a6241f86ca..8e00c64c9b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -346,7 +346,6 @@ data class AccountEntity( companion object { const val TABLE_NAME = "accounts" const val ACCOUNT_TYPE_GOOGLE = "com.google" - const val ACCOUNT_TYPE_GOOGLE_SIGN_NEW = "com.google" const val ACCOUNT_TYPE_OUTLOOK = "outlook.com" } } From 4a49d9eb4ab9b109bdb12e732f61089c61decf43 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 10:20:08 +0300 Subject: [PATCH 05/24] wip --- .../viewmodel/SignInWithGoogleViewModel.kt | 25 ++++++++++--------- .../activity/fragment/MainSignInFragment.kt | 24 ++++++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index 3f96bf6ab1..3647a2937a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -40,19 +40,19 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app googleIdTokenCredentialMutableStateFlow.value = Result.loading() googleIdTokenCredentialMutableStateFlow.value = controlledRunnerForGoogleIdTokenCredential.cancelPreviousThenRun { - val context: Context = getApplication() + try { + val context: Context = getApplication() - val nonce = UUID.randomUUID().toString() - val getSignInWithGoogleOption = - GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) - .setNonce(nonce) - .build() + val randomNonce = UUID.randomUUID().toString() + val getSignInWithGoogleOption = + GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) + .setNonce(randomNonce) + .build() - val getCredentialRequest = GetCredentialRequest.Builder() - .addCredentialOption(getSignInWithGoogleOption) - .build() + val getCredentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(getSignInWithGoogleOption) + .build() - try { val getCredentialResponse = CredentialManager.create(context).getCredential( context = context, request = getCredentialRequest @@ -67,12 +67,13 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app //mode details cab be found here https://auth0.com/docs/get-started/authentication-and-authorization-flow/implicit-flow-with-form-post/mitigate-replay-attacks-when-using-the-implicit-flow val idToken = googleIdTokenCredential.idToken val jwtConsumerBuilder = JwtConsumerBuilder() + //we don't need a verification. Just parse JWT and extract 'nonce' parameter .setSkipSignatureVerification() .setExpectedAudience(GoogleApiClientHelper.SERVER_CLIENT_ID) .build() val claims = jwtConsumerBuilder.processToClaims(idToken) - if (claims.getClaimValueAsString("nonce") != nonce) { - throw IllegalStateException("Security error: nonce mismatch") + if (claims.getClaimValueAsString("nonce") != randomNonce) { + throw IllegalStateException("Security error: 'nonce' mismatch") } return@cancelPreviousThenRun Result.success(googleIdTokenCredential) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 85527782c6..79f48a4cba 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -41,6 +41,7 @@ import com.flowcrypt.email.extensions.android.os.getParcelableViaExt import com.flowcrypt.email.extensions.android.os.getSerializableViaExt import com.flowcrypt.email.extensions.androidx.fragment.app.countingIdlingResource import com.flowcrypt.email.extensions.androidx.fragment.app.getNavigationResult +import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.setFragmentResultListenerForTwoWayDialog import com.flowcrypt.email.extensions.androidx.fragment.app.showFeedbackFragment @@ -144,6 +145,7 @@ class MainSignInFragment : BaseSingInFragment() { initEnterpriseViewModels() initPrivateKeysViewModel() initProtectPrivateKeysLiveData() + initSignInWithGoogleViewModel() } override fun getTempAccount(): AccountEntity? { @@ -773,6 +775,28 @@ class MainSignInFragment : BaseSingInFragment() { } } + private fun initSignInWithGoogleViewModel() { + launchAndRepeatWithViewLifecycle { + signInWithGoogleViewModel.googleIdTokenCredentialStateFlow.collect { + when (it.status) { + Result.Status.SUCCESS -> { + toast(it.data?.displayName) + //we can do authorization here + } + + Result.Status.EXCEPTION -> { + showInfoDialog( + dialogTitle = "", + dialogMsg = it.exceptionMsg + ) + } + + else -> {} + } + } + } + } + private fun continueBasedOnFlavorSettings(errorMsg: String) { @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { From ddab634f9c06ce53c2fb2e0f7d55ad50a5a359ec Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 10:24:10 +0300 Subject: [PATCH 06/24] wip --- .../viewmodel/SignInWithGoogleViewModel.kt | 4 + .../activity/fragment/MainSignInFragment.kt | 89 ++++++------------- 2 files changed, 29 insertions(+), 64 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index 3647a2937a..0a64067733 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -92,4 +92,8 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app } } } + + fun resetCachedAuthenticateState() { + googleIdTokenCredentialMutableStateFlow.value = Result.none() + } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 79f48a4cba..17526a32cf 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -16,13 +16,8 @@ import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.credentials.GetCredentialResponse import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import com.flowcrypt.email.BuildConfig import com.flowcrypt.email.Constants @@ -71,23 +66,17 @@ import com.flowcrypt.email.util.exception.CommonConnectionException import com.flowcrypt.email.util.exception.EkmNotSupportedException import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.UnsupportedClientConfigurationException -import com.flowcrypt.email.util.google.GoogleApiClientHelper import com.google.android.gms.auth.api.identity.AuthorizationRequest import com.google.android.gms.auth.api.identity.Identity import com.google.android.gms.common.api.Scope -import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.android.material.snackbar.Snackbar import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import com.sun.mail.util.MailConnectException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.pgpainless.util.Passphrase import java.net.HttpURLConnection import java.net.SocketTimeoutException -import java.util.UUID import javax.net.ssl.SSLException /** @@ -97,7 +86,6 @@ class MainSignInFragment : BaseSingInFragment() { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentMainSignInBinding.inflate(inflater, container, false) - private var cachedGoogleIdTokenCredential: GoogleIdTokenCredential? = null private var cachedClientConfiguration: ClientConfiguration? = null private var cachedBaseFesUrlPath: String? = null @@ -118,7 +106,7 @@ class MainSignInFragment : BaseSingInFragment() { ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK) { - signInWithGoogle() + binding?.buttonSignInWithGmail?.callOnClick() } } @@ -131,9 +119,12 @@ class MainSignInFragment : BaseSingInFragment() { override val isDisplayHomeAsUpEnabled: Boolean get() = false + val cachedGoogleIdTokenCredential: GoogleIdTokenCredential? + get() = signInWithGoogleViewModel.googleIdTokenCredentialStateFlow.value.data + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initViews(view) + initViews() subscribeToCheckAccountSettingsAndSearchBackups() subscribeToCheckPrivateKeys() @@ -182,15 +173,15 @@ class MainSignInFragment : BaseSingInFragment() { handleUnlockedKeys(accountEntity, keys) } - private fun initViews(view: View) { - view.findViewById(R.id.buttonSignInWithGmail)?.setOnClickListener { + private fun initViews() { + binding?.buttonSignInWithGmail?.setOnClickListener { cachedBaseFesUrlPath = null cachedClientConfiguration = null importCandidates.clear() - signInWithGoogle() + signInWithGoogleViewModel.authenticateUser() } - view.findViewById(R.id.buttonOtherEmailProvider)?.setOnClickListener { + binding?.buttonOtherEmailProvider?.setOnClickListener { cachedBaseFesUrlPath = null cachedClientConfiguration = null navController?.navigateSafe( @@ -199,15 +190,15 @@ class MainSignInFragment : BaseSingInFragment() { ) } - view.findViewById(R.id.buttonPrivacy)?.setOnClickListener { + binding?.buttonPrivacy?.setOnClickListener { GeneralUtil.openCustomTab(requireContext(), Constants.FLOWCRYPT_PRIVACY_URL) } - view.findViewById(R.id.buttonTerms)?.setOnClickListener { + binding?.buttonTerms?.setOnClickListener { GeneralUtil.openCustomTab(requireContext(), Constants.FLOWCRYPT_TERMS_URL) } - view.findViewById(R.id.buttonSecurity)?.setOnClickListener { + binding?.buttonSecurity?.setOnClickListener { navController?.navigateSafe( R.id.mainSignInFragment, NavGraphDirections.actionGlobalHtmlViewFromAssetsRawFragment( @@ -217,51 +208,11 @@ class MainSignInFragment : BaseSingInFragment() { ) } - view.findViewById(R.id.buttonHelp)?.setOnClickListener { + binding?.buttonHelp?.setOnClickListener { showFeedbackFragment() } } - private fun signInWithGoogle() { - signInWithGoogleViewModel.authenticateUser() - return - - cachedGoogleIdTokenCredential = null - - val getSignInWithGoogleOption = - GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) - //need to think about nonce more - .setNonce(UUID.randomUUID().toString()) - .build() - - val getCredentialRequest = GetCredentialRequest.Builder() - .addCredentialOption(getSignInWithGoogleOption) - .build() - - //need to test it with slow internet. Maybe need to use a dialog here - lifecycleScope.launch { - try { - val getCredentialResponse = CredentialManager.create(requireContext()).getCredential( - context = requireContext(), - request = getCredentialRequest - ) - withContext(Dispatchers.Main) { - handleAuthentication(getCredentialResponse) - } - } catch (e: Exception) { - e.printStackTraceIfDebugOnly() - //need to test it - withContext(Dispatchers.Main) { - showInfoDialog( - dialogTitle = "", - dialogMsg = e.message ?: getString(R.string.unknown_error), - isCancelable = true - ) - } - } - } - } - private fun handleAuthentication(getCredentialResponse: GetCredentialResponse) { when (val credential = getCredentialResponse.credential) { is CustomCredential -> { @@ -780,8 +731,15 @@ class MainSignInFragment : BaseSingInFragment() { signInWithGoogleViewModel.googleIdTokenCredentialStateFlow.collect { when (it.status) { Result.Status.SUCCESS -> { - toast(it.data?.displayName) - //we can do authorization here + if (it.data != null) { + authorizeUserToGmailApi(it.data) + } else { + signInWithGoogleViewModel.resetCachedAuthenticateState() + showInfoDialog( + dialogTitle = "", + dialogMsg = getString(R.string.error_occurred_try_again_later) + ) + } } Result.Status.EXCEPTION -> { @@ -789,6 +747,9 @@ class MainSignInFragment : BaseSingInFragment() { dialogTitle = "", dialogMsg = it.exceptionMsg ) + + (it.exception as? Exception)?.printStackTraceIfDebugOnly() + signInWithGoogleViewModel.resetCachedAuthenticateState() } else -> {} From a877fb580c3f3addd9a61b35a2f5e0388689d278 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 11:59:54 +0300 Subject: [PATCH 07/24] wip --- .../extensions/java/lang/ExceptionExt.kt | 13 + .../activity/fragment/MainSignInFragment.kt | 228 +++++++----------- 2 files changed, 100 insertions(+), 141 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/java/lang/ExceptionExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/java/lang/ExceptionExt.kt index a2460479f2..38988ade0a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/java/lang/ExceptionExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/java/lang/ExceptionExt.kt @@ -5,6 +5,9 @@ package com.flowcrypt.email.extensions.java.lang +import androidx.fragment.app.Fragment +import com.flowcrypt.email.R +import com.flowcrypt.email.extensions.androidx.fragment.app.showInfoDialog import com.flowcrypt.email.util.GeneralUtil /** @@ -15,3 +18,13 @@ fun Exception.printStackTraceIfDebugOnly() { printStackTrace() } } + +fun Exception.showDialogWithErrorDetails(fragment: Fragment) { + fragment.showInfoDialog( + dialogTitle = "", + dialogMsg = fragment.getString( + R.string.error_occurred_with_details_please_try_again, + localizedMessage + ) + ) +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 17526a32cf..62f8fbb075 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -8,7 +8,6 @@ package com.flowcrypt.email.ui.activity.fragment import android.app.Activity import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -16,6 +15,7 @@ import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.navigation.NavDirections @@ -48,6 +48,7 @@ import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.exceptionMsg import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly +import com.flowcrypt.email.extensions.java.lang.showDialogWithErrorDetails import com.flowcrypt.email.jetpack.viewmodel.CheckCustomerUrlFesServerViewModel import com.flowcrypt.email.jetpack.viewmodel.ClientConfigurationViewModel import com.flowcrypt.email.jetpack.viewmodel.EkmViewModel @@ -55,7 +56,6 @@ import com.flowcrypt.email.jetpack.viewmodel.SignInWithGoogleViewModel import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.model.PgpKeyRingDetails import com.flowcrypt.email.service.CheckClipboardToFindKeyService -import com.flowcrypt.email.service.CheckClipboardToFindKeyService.Companion.TAG import com.flowcrypt.email.ui.activity.fragment.CheckKeysFragment.CheckingState.Companion.CHECKED_KEYS import com.flowcrypt.email.ui.activity.fragment.CheckKeysFragment.CheckingState.Companion.SKIP_REMAINING_KEYS import com.flowcrypt.email.ui.activity.fragment.base.BaseSingInFragment @@ -67,10 +67,10 @@ import com.flowcrypt.email.util.exception.EkmNotSupportedException import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.UnsupportedClientConfigurationException import com.google.android.gms.auth.api.identity.AuthorizationRequest +import com.google.android.gms.auth.api.identity.AuthorizationResult import com.google.android.gms.auth.api.identity.Identity import com.google.android.gms.common.api.Scope import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.android.material.snackbar.Snackbar import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import com.sun.mail.util.MailConnectException @@ -98,8 +98,14 @@ class MainSignInFragment : BaseSingInFragment() { private val forActivityResultSignIn = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() ) { result: ActivityResult -> - toast("autorized!") - //handleSignIn(result.resultCode) + if (result.resultCode == Activity.RESULT_OK) { + val authorizationResult = Identity.getAuthorizationClient( + requireContext() + ).getAuthorizationResultFromIntent(result.data) + onUserAuthorizedToGmailApi(authorizationResult) + } else { + signInWithGoogleViewModel.resetCachedAuthenticateState() + } } private val forActivityResultSignInError = registerForActivityResult( @@ -119,7 +125,7 @@ class MainSignInFragment : BaseSingInFragment() { override val isDisplayHomeAsUpEnabled: Boolean get() = false - val cachedGoogleIdTokenCredential: GoogleIdTokenCredential? + private val cachedGoogleIdTokenCredential: GoogleIdTokenCredential? get() = signInWithGoogleViewModel.googleIdTokenCredentialStateFlow.value.data override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -213,141 +219,71 @@ class MainSignInFragment : BaseSingInFragment() { } } - private fun handleAuthentication(getCredentialResponse: GetCredentialResponse) { - when (val credential = getCredentialResponse.credential) { - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - try { - cachedGoogleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) - val account = cachedGoogleIdTokenCredential?.id ?: return - cachedBaseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false) + private fun handleAuthentication(googleIdTokenCredential: GoogleIdTokenCredential) { + val account = googleIdTokenCredential.id + cachedBaseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false) - val publicEmailDomains = EmailUtil.getPublicEmailDomains() - val domain = EmailUtil.getDomain(account) - if (domain in publicEmailDomains) { - @Suppress("KotlinConstantConditions") - if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { - cachedGoogleIdTokenCredential = null - showInfoDialog( - dialogTitle = "", - dialogMsg = getString( - R.string.enterprise_does_not_support_pub_domains, - getString(R.string.app_name), - domain - ), - isCancelable = true - ) - } else { - val idToken = cachedGoogleIdTokenCredential?.idToken - if (idToken == null) { - showInfoDialog( - dialogTitle = "", - dialogMsg = getString( - R.string.error_occurred_with_details_please_try_again, - "GoogleIdTokenCredential.idToken == null" - ), - isCancelable = true - ) - } else { - val authorizationRequest: AuthorizationRequest = AuthorizationRequest.builder() - .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))).build() - Identity.getAuthorizationClient(requireContext()) - .authorize(authorizationRequest) - .addOnSuccessListener { authorizationResult -> - if (authorizationResult.hasResolution()) { - // Access needs to be granted by the user. - // At this stage the grant access to Gmail API screen should be displayed - val pendingIntent = authorizationResult.pendingIntent - - if (pendingIntent != null) { - try { - forActivityResultSignIn.launch( - IntentSenderRequest.Builder(pendingIntent.intentSender).build() - ) - } catch (e: Exception) { - //need to handle this case - Log.e(TAG, "Couldn't start Authorization UI: " + e.localizedMessage) - } - } else { - //need to handle this case - } - } else { - val oldToken = idToken - val newToken = authorizationResult.toGoogleSignInAccount()?.idToken - val accesToken = authorizationResult.accessToken - - println(oldToken + newToken + accesToken) - // Access already granted, continue with user action - clientConfigurationViewModel.fetchClientConfiguration( - idToken = accesToken!!, - baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), - domain = domain - ) - } - }.addOnFailureListener { exception -> - toast("addOnFailureListener") - Log.e( - TAG, - "Failed to authorize", - exception - ) - } - - /*clientConfigurationViewModel.fetchClientConfiguration( - idToken = idToken, - baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), - domain = domain - )*/ - } - } - } else { - val authorizationRequest: AuthorizationRequest = AuthorizationRequest.builder() - .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))).build() - Identity.getAuthorizationClient(requireContext()) - .authorize(authorizationRequest) - .addOnSuccessListener { authorizationResult -> - if (authorizationResult.hasResolution()) { - // Access needs to be granted by the user. - // At this stage the grant access to Gmail API screen should be displayed - val pendingIntent = authorizationResult.pendingIntent - - if (pendingIntent != null) { - try { - forActivityResultSignIn.launch( - IntentSenderRequest.Builder(pendingIntent.intentSender).build() - ) - } catch (e: Exception) { - //need to handle this case - Log.e(TAG, "Couldn't start Authorization UI: " + e.localizedMessage) - } - } else { - //need to handle this case - } - } else { - // Access already granted, continue with user action - checkCustomerUrlFesServerViewModel.checkServerAvailability(account) - } - }.addOnFailureListener { exception -> - toast("addOnFailureListener") - Log.e( - TAG, - "Failed to authorize", - exception - ) - } + val publicEmailDomains = EmailUtil.getPublicEmailDomains() + val domain = EmailUtil.getDomain(account) + if (domain in publicEmailDomains) { + @Suppress("KotlinConstantConditions") + if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { + signInWithGoogleViewModel.resetCachedAuthenticateState() + showInfoDialog( + dialogTitle = "", + dialogMsg = getString( + R.string.enterprise_does_not_support_pub_domains, + getString(R.string.app_name), + domain + ), + isCancelable = true + ) + } else { + authorizeUserToGmailApi() + } + } else { + authorizeUserToGmailApi() + } + } + + private fun authorizeUserToGmailApi() { + val authorizationRequest: AuthorizationRequest = AuthorizationRequest.builder() + .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))).build() + Identity.getAuthorizationClient(requireContext()) + .authorize(authorizationRequest) + .addOnSuccessListener { authorizationResult -> + if (authorizationResult.hasResolution()) { + // Access needs to be granted by the user. + // At this stage the grant access to Gmail API screen should be displayed + val pendingIntent = authorizationResult.pendingIntent + if (pendingIntent != null) { + try { + forActivityResultSignIn.launch( + IntentSenderRequest.Builder(pendingIntent.intentSender).build() + ) + } catch (e: Exception) { + e.showDialogWithErrorDetails(this) } - } catch (e: GoogleIdTokenParsingException) { - Log.e(TAG, "Received an invalid google id token response", e) + } else { + showInfoDialog( + dialogTitle = "", + dialogMsg = getString( + R.string.error_occurred_with_details_please_try_again, + "pendingIntent == null" + ) + ) } } else { - toast("Unsupported credentials") + // Access already granted, continue with user action + onUserAuthorizedToGmailApi(authorizationResult) } + }.addOnFailureListener { exception -> + exception.showDialogWithErrorDetails(this) } + } - else -> { - toast("Unsupported credentials") - } - } + private fun onUserAuthorizedToGmailApi(authorizationResult: AuthorizationResult) { + toast("$authorizationResult") } private fun onSignSuccess(googleIdTokenCredential: GoogleIdTokenCredential?) { @@ -571,7 +507,7 @@ class MainSignInFragment : BaseSingInFragment() { } CreateOrImportPrivateKeyDuringSetupFragment.Result.USE_ANOTHER_ACCOUNT -> { - this.cachedGoogleIdTokenCredential = null + //this.cachedGoogleIdTokenCredential = null showContent() } } @@ -732,21 +668,31 @@ class MainSignInFragment : BaseSingInFragment() { when (it.status) { Result.Status.SUCCESS -> { if (it.data != null) { - authorizeUserToGmailApi(it.data) + handleAuthentication(it.data) } else { signInWithGoogleViewModel.resetCachedAuthenticateState() showInfoDialog( dialogTitle = "", - dialogMsg = getString(R.string.error_occurred_try_again_later) + dialogMsg = getString( + R.string.error_occurred_with_details_please_try_again, + "data == null" + ) ) } } Result.Status.EXCEPTION -> { - showInfoDialog( - dialogTitle = "", - dialogMsg = it.exceptionMsg - ) + when { + it.exception is GetCredentialCancellationException + && "android.credentials.GetCredentialException.TYPE_USER_CANCELED" == it.exception.type -> { + //do nothing + } + + else -> showInfoDialog( + dialogTitle = "", + dialogMsg = it.exceptionMsg + ) + } (it.exception as? Exception)?.printStackTraceIfDebugOnly() signInWithGoogleViewModel.resetCachedAuthenticateState() From fe999289c6c09994640ed885ac4dcfcd699f2ee8 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 12:19:21 +0300 Subject: [PATCH 08/24] wip --- .../api/retrofit/response/base/Result.kt | 7 ++++- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 2 +- .../viewmodel/SignInWithGoogleViewModel.kt | 5 ++++ .../activity/fragment/MainSignInFragment.kt | 26 +++++++++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt index a8fb046d7c..372176cb3b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt @@ -24,12 +24,17 @@ data class Result( val progress: Double? = null ) : Serializable { + fun toCached(): Result { + return copy(status = Status.SUCCESS_CACHED) + } + enum class Status { SUCCESS, ERROR, EXCEPTION, LOADING, - NONE + NONE, + SUCCESS_CACHED } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 942b6f842f..033f509047 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -261,7 +261,7 @@ class MsgDetailsViewModel( ) } - Result.Status.NONE -> { + else -> { Result.none() } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index 0a64067733..f57dca62ee 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -96,4 +96,9 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app fun resetCachedAuthenticateState() { googleIdTokenCredentialMutableStateFlow.value = Result.none() } + + fun cacheAuthenticateState() { + googleIdTokenCredentialMutableStateFlow.value = + googleIdTokenCredentialMutableStateFlow.value.toCached() + } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 62f8fbb075..2223b974ef 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -283,7 +283,29 @@ class MainSignInFragment : BaseSingInFragment() { } private fun onUserAuthorizedToGmailApi(authorizationResult: AuthorizationResult) { - toast("$authorizationResult") + val account = cachedGoogleIdTokenCredential?.id + val idToken = cachedGoogleIdTokenCredential?.idToken + + if (idToken == null) { + IllegalStateException("idToken == null").showDialogWithErrorDetails(this) + return + } + + if (account != null) { + val publicEmailDomains = EmailUtil.getPublicEmailDomains() + val domain = EmailUtil.getDomain(account) + if (domain in publicEmailDomains) { + clientConfigurationViewModel.fetchClientConfiguration( + idToken = idToken, + baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), + domain = domain + ) + } else { + checkCustomerUrlFesServerViewModel.checkServerAvailability(account) + } + } else { + IllegalStateException("account == null").showDialogWithErrorDetails(this) + } } private fun onSignSuccess(googleIdTokenCredential: GoogleIdTokenCredential?) { @@ -670,7 +692,6 @@ class MainSignInFragment : BaseSingInFragment() { if (it.data != null) { handleAuthentication(it.data) } else { - signInWithGoogleViewModel.resetCachedAuthenticateState() showInfoDialog( dialogTitle = "", dialogMsg = getString( @@ -679,6 +700,7 @@ class MainSignInFragment : BaseSingInFragment() { ) ) } + signInWithGoogleViewModel.cacheAuthenticateState() } Result.Status.EXCEPTION -> { From 9f43e568c66a6a697824c75bcc9cae75d4dfc3c1 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 12:26:45 +0300 Subject: [PATCH 09/24] wip --- .../email/ui/activity/fragment/MainSignInFragment.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 2223b974ef..a0c14fcd85 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -99,10 +99,7 @@ class MainSignInFragment : BaseSingInFragment() { ActivityResultContracts.StartIntentSenderForResult() ) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK) { - val authorizationResult = Identity.getAuthorizationClient( - requireContext() - ).getAuthorizationResultFromIntent(result.data) - onUserAuthorizedToGmailApi(authorizationResult) + onUserAuthorizedToGmailApi() } else { signInWithGoogleViewModel.resetCachedAuthenticateState() } @@ -275,15 +272,15 @@ class MainSignInFragment : BaseSingInFragment() { } } else { // Access already granted, continue with user action - onUserAuthorizedToGmailApi(authorizationResult) + onUserAuthorizedToGmailApi() } }.addOnFailureListener { exception -> exception.showDialogWithErrorDetails(this) } } - private fun onUserAuthorizedToGmailApi(authorizationResult: AuthorizationResult) { - val account = cachedGoogleIdTokenCredential?.id + private fun onUserAuthorizedToGmailApi() { + val account = cachedGoogleIdTokenCredential?.id?.lowercase() val idToken = cachedGoogleIdTokenCredential?.idToken if (idToken == null) { From 2c0ce0f6c34b323e44af67cc8ce16e4b151beb16 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 12:35:34 +0300 Subject: [PATCH 10/24] wip --- .../jetpack/viewmodel/SignInWithGoogleViewModel.kt | 14 ++++++++++++++ .../ui/activity/fragment/MainSignInFragment.kt | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index f57dca62ee..e7b9afa220 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.viewModelScope import com.flowcrypt.email.R import com.flowcrypt.email.api.retrofit.response.base.Result import com.flowcrypt.email.util.coroutines.runners.ControlledRunner +import com.flowcrypt.email.util.exception.AccountAlreadyAddedException import com.flowcrypt.email.util.google.GoogleApiClientHelper import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential @@ -76,6 +77,19 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app throw IllegalStateException("Security error: 'nonce' mismatch") } + val existedAccount = roomDatabase.accountDao().getAccountsSuspend().firstOrNull { + it.email.equals(googleIdTokenCredential.id, ignoreCase = true) + } + + if (existedAccount != null) { + throw AccountAlreadyAddedException( + context.getString( + R.string.template_email_already_added, + existedAccount.email + ) + ) + } + return@cancelPreviousThenRun Result.success(googleIdTokenCredential) } else { throw IllegalStateException(context.getString(R.string.unsupported_credentials)) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index a0c14fcd85..db0cdd8744 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -67,7 +67,6 @@ import com.flowcrypt.email.util.exception.EkmNotSupportedException import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.UnsupportedClientConfigurationException import com.google.android.gms.auth.api.identity.AuthorizationRequest -import com.google.android.gms.auth.api.identity.AuthorizationResult import com.google.android.gms.auth.api.identity.Identity import com.google.android.gms.common.api.Scope import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential @@ -707,6 +706,10 @@ class MainSignInFragment : BaseSingInFragment() { //do nothing } + it.exception is AccountAlreadyAddedException -> { + toast(it.exception.message) + } + else -> showInfoDialog( dialogTitle = "", dialogMsg = it.exceptionMsg From a1bc62f86c2bae8cdf54c7d61d45c493f800279f Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 12:43:53 +0300 Subject: [PATCH 11/24] wip --- .../email/jetpack/viewmodel/SignInWithGoogleViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index e7b9afa220..f79194c050 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -77,6 +77,7 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app throw IllegalStateException("Security error: 'nonce' mismatch") } + //check that the given account is new, not added yet val existedAccount = roomDatabase.accountDao().getAccountsSuspend().firstOrNull { it.email.equals(googleIdTokenCredential.id, ignoreCase = true) } From 3238b878cabe652806e55c483070067eb30753e4 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 12:45:25 +0300 Subject: [PATCH 12/24] wip --- .../email/jetpack/viewmodel/SignInWithGoogleViewModel.kt | 4 ++-- .../email/ui/activity/fragment/MainSignInFragment.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index f79194c050..6f58f1526b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -108,11 +108,11 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app } } - fun resetCachedAuthenticateState() { + fun resetAuthenticationState() { googleIdTokenCredentialMutableStateFlow.value = Result.none() } - fun cacheAuthenticateState() { + fun cacheAuthenticationState() { googleIdTokenCredentialMutableStateFlow.value = googleIdTokenCredentialMutableStateFlow.value.toCached() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index db0cdd8744..c56ce64085 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -100,7 +100,7 @@ class MainSignInFragment : BaseSingInFragment() { if (result.resultCode == Activity.RESULT_OK) { onUserAuthorizedToGmailApi() } else { - signInWithGoogleViewModel.resetCachedAuthenticateState() + signInWithGoogleViewModel.resetAuthenticationState() } } @@ -224,7 +224,7 @@ class MainSignInFragment : BaseSingInFragment() { if (domain in publicEmailDomains) { @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { - signInWithGoogleViewModel.resetCachedAuthenticateState() + signInWithGoogleViewModel.resetAuthenticationState() showInfoDialog( dialogTitle = "", dialogMsg = getString( @@ -696,7 +696,7 @@ class MainSignInFragment : BaseSingInFragment() { ) ) } - signInWithGoogleViewModel.cacheAuthenticateState() + signInWithGoogleViewModel.cacheAuthenticationState() } Result.Status.EXCEPTION -> { @@ -717,7 +717,7 @@ class MainSignInFragment : BaseSingInFragment() { } (it.exception as? Exception)?.printStackTraceIfDebugOnly() - signInWithGoogleViewModel.resetCachedAuthenticateState() + signInWithGoogleViewModel.resetAuthenticationState() } else -> {} From 404fb79ee898a0bd4bee1f414b48a7d304bbe7f3 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 6 Jun 2024 12:51:17 +0300 Subject: [PATCH 13/24] wip --- .../flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index c56ce64085..7bc773ab42 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -525,7 +525,7 @@ class MainSignInFragment : BaseSingInFragment() { } CreateOrImportPrivateKeyDuringSetupFragment.Result.USE_ANOTHER_ACCOUNT -> { - //this.cachedGoogleIdTokenCredential = null + signInWithGoogleViewModel.resetAuthenticationState() showContent() } } From 3e9a35dbe84d5b4942c02148eb3323e07d489b0d Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 10 Jun 2024 17:38:42 +0300 Subject: [PATCH 14/24] wip --- FlowCrypt/build.gradle.kts | 2 +- .../main/java/com/flowcrypt/email/ui/activity/MainActivity.kt | 1 - .../flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/build.gradle.kts b/FlowCrypt/build.gradle.kts index ab86e10434..380a70981b 100644 --- a/FlowCrypt/build.gradle.kts +++ b/FlowCrypt/build.gradle.kts @@ -449,7 +449,7 @@ dependencies { implementation("androidx.credentials:credentials-play-services-auth:1.2.2") implementation("com.google.android.gms:play-services-base:18.5.0") - implementation("com.google.android.gms:play-services-auth:21.2.0")//should be removed + implementation("com.google.android.gms:play-services-auth:21.2.0") implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("com.google.android.libraries.identity.googleid:googleid:1.1.0") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt index 5271edb47c..6664acc6c4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt @@ -398,7 +398,6 @@ class MainActivity : BaseActivity() { ClearCredentialStateRequest() ) } catch (e: ClearCredentialException) { - //need to test bad connection. Maybe it will be better to use dialog here. e.printStackTraceIfDebugOnly() showInfoDialog( requestKey = UUID.randomUUID().toString(), diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 7bc773ab42..212ad29bf7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -702,7 +702,8 @@ class MainSignInFragment : BaseSingInFragment() { Result.Status.EXCEPTION -> { when { it.exception is GetCredentialCancellationException - && "android.credentials.GetCredentialException.TYPE_USER_CANCELED" == it.exception.type -> { + && "android.credentials.GetCredentialException.TYPE_USER_CANCELED" == it.exception.type + && "[16] Cancelled by user." == it.exception.errorMessage -> { //do nothing } From 4b9d5d44edc1dae99ef3985f026f18fc3e913c72 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 11 Jun 2024 11:45:55 +0300 Subject: [PATCH 15/24] wip --- .../jetpack/viewmodel/SignInWithGoogleViewModel.kt | 14 ++++++-------- .../ui/activity/fragment/MainSignInFragment.kt | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt index 6f58f1526b..eb47967907 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt @@ -36,14 +36,12 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app val googleIdTokenCredentialStateFlow: StateFlow> = googleIdTokenCredentialMutableStateFlow.asStateFlow() - fun authenticateUser() { + fun authenticateUser(activityContext: Context) { viewModelScope.launch { googleIdTokenCredentialMutableStateFlow.value = Result.loading() googleIdTokenCredentialMutableStateFlow.value = controlledRunnerForGoogleIdTokenCredential.cancelPreviousThenRun { try { - val context: Context = getApplication() - val randomNonce = UUID.randomUUID().toString() val getSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) @@ -54,8 +52,8 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app .addCredentialOption(getSignInWithGoogleOption) .build() - val getCredentialResponse = CredentialManager.create(context).getCredential( - context = context, + val getCredentialResponse = CredentialManager.create(activityContext).getCredential( + context = activityContext, request = getCredentialRequest ) @@ -84,7 +82,7 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app if (existedAccount != null) { throw AccountAlreadyAddedException( - context.getString( + activityContext.getString( R.string.template_email_already_added, existedAccount.email ) @@ -93,12 +91,12 @@ class SignInWithGoogleViewModel(application: Application) : AccountViewModel(app return@cancelPreviousThenRun Result.success(googleIdTokenCredential) } else { - throw IllegalStateException(context.getString(R.string.unsupported_credentials)) + throw IllegalStateException(activityContext.getString(R.string.unsupported_credentials)) } } else -> { - throw IllegalStateException(context.getString(R.string.unsupported_credentials)) + throw IllegalStateException(activityContext.getString(R.string.unsupported_credentials)) } } } catch (e: Exception) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 212ad29bf7..aa7917c211 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -180,7 +180,7 @@ class MainSignInFragment : BaseSingInFragment() { cachedBaseFesUrlPath = null cachedClientConfiguration = null importCandidates.clear() - signInWithGoogleViewModel.authenticateUser() + signInWithGoogleViewModel.authenticateUser(requireActivity()) } binding?.buttonOtherEmailProvider?.setOnClickListener { @@ -703,7 +703,7 @@ class MainSignInFragment : BaseSingInFragment() { when { it.exception is GetCredentialCancellationException && "android.credentials.GetCredentialException.TYPE_USER_CANCELED" == it.exception.type - && "[16] Cancelled by user." == it.exception.errorMessage -> { + && isConnected() -> { //do nothing } From de43638c78da351ff1f53593df19b73598736974 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 11 Jun 2024 13:20:38 +0300 Subject: [PATCH 16/24] wip --- .../com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt index ebe0a01178..2bd9cce835 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt @@ -23,6 +23,7 @@ import com.flowcrypt.email.util.exception.ApiException import com.google.gson.Gson import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -35,6 +36,9 @@ import java.net.HttpURLConnection */ @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore( + "Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/" +) class FesDuringSetupCommonFlowTest : BaseFesDuringSetupFlowTest() { @get:Rule From 6cad720c4409b67c9851bf2f5006d60b7d622425 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 11 Jun 2024 14:09:48 +0300 Subject: [PATCH 17/24] wip --- .../java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt | 7 +------ script/ci-junit-tests.sh | 2 +- script/ci-lint-checks.sh | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt index 66a121cb51..5280c4bb97 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpMsgTest.kt @@ -156,9 +156,8 @@ class PgpMsgTest { "decrypt - [gpg] signed fully armored message" ) for (key in keys) { - println("Decrypt: '$key'") val r = processMessage(key) - assertTrue("Message not returned", r.content != null) + assertTrue("Message not returned for $key", r.content != null) val messageInfo = findMessage(key) checkContent( expected = messageInfo.content, @@ -299,10 +298,6 @@ class PgpMsgTest { secretKeys = PGPSecretKeyRingCollection(PRIVATE_KEYS.map { it.keyRing }), protector = secretKeyRingProtector ) - if (result.content != null) { - val s = String(result.content!!.toByteArray(), Charset.forName(messageInfo.charset)) - println("=========\n$s\n=========") - } return result } diff --git a/script/ci-junit-tests.sh b/script/ci-junit-tests.sh index dfdcb6a2ec..94cb315567 100755 --- a/script/ci-junit-tests.sh +++ b/script/ci-junit-tests.sh @@ -1,3 +1,3 @@ #!/bin/bash -./gradlew --console=plain :FlowCrypt:testConsumerUiTestsUnitTest +./gradlew --console=plain :FlowCrypt:testConsumerUiTestsUnitTest --rerun-tasks diff --git a/script/ci-lint-checks.sh b/script/ci-lint-checks.sh index cdd01071ab..1861bfa44f 100755 --- a/script/ci-lint-checks.sh +++ b/script/ci-lint-checks.sh @@ -1,3 +1,3 @@ #!/bin/bash -./gradlew --console=plain :FlowCrypt:lintConsumerUiTests +./gradlew --console=plain :FlowCrypt:lintConsumerUiTests --rerun-tasks From bf450afce5960db30af8229def1ec84a5b504d0c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 11 Jun 2024 14:52:58 +0300 Subject: [PATCH 18/24] wip --- .../flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt index 13139ec5c5..8fc147db4d 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt @@ -35,6 +35,7 @@ import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.not +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -50,6 +51,9 @@ import java.util.concurrent.TimeUnit @MediumTest @RunWith(AndroidJUnit4::class) @EnterpriseTest +@Ignore( + "Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/" +) class FesDuringSetupEnterpriseFlowTest : BaseFesDuringSetupFlowTest() { @get:Rule var ruleChain: TestRule = RuleChain From 49fce885de7d4e1d2247b6fefcb792676236d84a Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 12 Jun 2024 11:34:34 +0300 Subject: [PATCH 19/24] Disabled some tests.| #2531 --- .../com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt | 2 ++ .../com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt | 2 ++ .../java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt | 2 ++ .../java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt | 2 ++ ...ubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt | 2 ++ .../login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt | 2 ++ 6 files changed, 12 insertions(+) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt index 5c170c78ac..56813bec1a 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt @@ -31,6 +31,7 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.not import org.junit.ClassRule +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -44,6 +45,7 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class AddNewAccountEnterpriseFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt index 5600c08c30..73d4bcc0e8 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt @@ -28,6 +28,7 @@ import com.flowcrypt.email.util.exception.ApiException import com.google.gson.Gson import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -41,6 +42,7 @@ import java.util.concurrent.TimeUnit */ @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class FesDuringSetupConsumerFlowTest : BaseFesDuringSetupFlowTest() { @get:Rule diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt index 2261f48457..db05a6e27c 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt @@ -50,6 +50,7 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.not +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -64,6 +65,7 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class MainSignInFragmentFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule( TestGeneralUtil.genIntentForNavigationComponent( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt index 2bc17ee79f..df1382f8e8 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt @@ -25,6 +25,7 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import org.hamcrest.Matchers.allOf +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -39,6 +40,7 @@ import org.junit.runner.RunWith @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class SignInScreenFlowTest : BaseTest() { override val activityScenarioRule = activityScenarioRule() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt index 2468646e36..f65a5eb1da 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt @@ -42,6 +42,7 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Before import org.junit.ClassRule +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -58,6 +59,7 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule( TestGeneralUtil.genIntentForNavigationComponent( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt index 3ff680384a..aa1f8f6d56 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt @@ -27,6 +27,7 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -40,6 +41,7 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) +@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class MainSignInFragmentEnterpriseTestUseFesUrlFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule( TestGeneralUtil.genIntentForNavigationComponent( From 6ed590c123b25eb7a614a17dc2f3eedabcd67116 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 13 Jun 2024 12:44:44 +0300 Subject: [PATCH 20/24] wip --- .../java/com/flowcrypt/email/util/TestGeneralUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/TestGeneralUtil.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/TestGeneralUtil.kt index 6d5e10b59e..286e8c7dc4 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/TestGeneralUtil.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/util/TestGeneralUtil.kt @@ -167,7 +167,7 @@ object TestGeneralUtil { fun replaceVersionInKey(key: String?): String { val regex = - "Version: FlowCrypt Email Encryption \\d*.\\d*.\\d*(_.*)?".toRegex() + "^Version: FlowCrypt Email Encryption .*\$".toRegex(RegexOption.MULTILINE) val version = BuildConfig.VERSION_NAME val replacement = "Version: FlowCrypt Email Encryption $version" key?.let { From 4f24e7a33af932f3fbc4847a02d6079dd2bf19f9 Mon Sep 17 00:00:00 2001 From: Denys Bondarenko Date: Thu, 10 Sep 2026 10:31:45 +0300 Subject: [PATCH 21/24] Migrated to Credential Manager and play-services-auth 22.0.0. A draft version. --- FlowCrypt/build.gradle.kts | 2 +- .../flowcrypt/email/ui/base/BaseSignTest.kt | 35 ++- .../flowcrypt/email/util/FlavorSettings.kt | 7 + .../flowcrypt/email/util/FlavorSettings.kt | 7 + .../email/database/entity/AccountEntity.kt | 5 +- .../email/ui/activity/MainActivity.kt | 17 +- .../activity/fragment/MainSignInFragment.kt | 266 +++++++++--------- .../email/util/EnvironmentSettings.kt | 2 + .../com/flowcrypt/email/util/GeneralUtil.kt | 28 +- .../email/util/acra/CustomReportSender.kt | 17 +- .../util/google/GoogleApiClientHelper.kt | 21 +- 11 files changed, 202 insertions(+), 205 deletions(-) diff --git a/FlowCrypt/build.gradle.kts b/FlowCrypt/build.gradle.kts index 1dc244b7b0..c33b3dfb36 100644 --- a/FlowCrypt/build.gradle.kts +++ b/FlowCrypt/build.gradle.kts @@ -514,7 +514,7 @@ dependencies { implementation("androidx.credentials:credentials-play-services-auth:1.6.0") implementation("com.google.android.gms:play-services-base:18.10.1") - implementation("com.google.android.gms:play-services-auth:21.6.0") + implementation("com.google.android.gms:play-services-auth:22.0.0") implementation("com.google.android.material:material:1.14.0") implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("com.google.android.libraries.identity.googleid:googleid:1.2.0") diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt index ba48d32f0d..628feff269 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt @@ -5,27 +5,23 @@ package com.flowcrypt.email.ui.base -import android.app.Activity -import android.app.Instrumentation -import android.content.Intent +import android.net.Uri import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.intent.Intents.intending -import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import com.flowcrypt.email.R import com.flowcrypt.email.base.BaseTest -import com.flowcrypt.email.util.google.GoogleApiClientHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.flowcrypt.email.util.FlavorSettings +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import okhttp3.mockwebserver.RecordedRequest import org.jose4j.jwa.AlgorithmConstraints import org.jose4j.jws.AlgorithmIdentifiers import org.jose4j.jws.JsonWebSignature import org.jose4j.jwt.JwtClaims import org.jose4j.jwt.consumer.JwtConsumerBuilder +import org.json.JSONObject /** @@ -34,16 +30,19 @@ import org.jose4j.jwt.consumer.JwtConsumerBuilder abstract class BaseSignTest : BaseTest() { protected fun setupAndClickSignInButton(signInAccountJson: String) { - val intent = Intent() - intent.putExtra("googleSignInAccount", GoogleSignInAccount.zaa(signInAccountJson)) - - val signInIntent = GoogleSignIn.getClient( - getTargetContext(), - GoogleApiClientHelper.generateGoogleSignInOptions() - ).signInIntent - - intending(hasComponent(signInIntent.component)) - .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, intent)) + val jsonObject = JSONObject(signInAccountJson) + FlavorSettings.setGoogleIdTokenCredential( + GoogleIdTokenCredential( + id = jsonObject.getString("email"), + idToken = jsonObject.getString("tokenId"), + displayName = jsonObject.optString("displayName").ifEmpty { null }, + familyName = jsonObject.optString("familyName").ifEmpty { null }, + givenName = jsonObject.optString("givenName").ifEmpty { null }, + profilePictureUri = jsonObject.optString("photoUrl").takeIf { it.isNotEmpty() } + ?.let(Uri::parse), + phoneNumber = null + ) + ) onView(withId(R.id.buttonSignInWithGmail)) .check(matches(isDisplayed())) diff --git a/FlowCrypt/src/consumerUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt b/FlowCrypt/src/consumerUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt index d54516a4d9..1fd7b3587e 100644 --- a/FlowCrypt/src/consumerUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt +++ b/FlowCrypt/src/consumerUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt @@ -7,12 +7,15 @@ package com.flowcrypt.email.util import android.content.Context import androidx.test.espresso.idling.CountingIdlingResource +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import java.util.UUID /** * @author Denys Bondarenko */ object FlavorSettings : EnvironmentSettings { + private var cachedGoogleIdTokenCredential: GoogleIdTokenCredential? = null + private val countingIdlingResource: CountingIdlingResource = CountingIdlingResource( GeneralUtil.genIdlingResourcesName(this::class.java), GeneralUtil.isDebugBuild() @@ -20,6 +23,10 @@ object FlavorSettings : EnvironmentSettings { override fun configure(context: Context) {} override fun getCountingIdlingResource() = countingIdlingResource override fun getGoogleIdToken(): String = UUID.randomUUID().toString() + override fun getGoogleIdTokenCredential(): GoogleIdTokenCredential? = cachedGoogleIdTokenCredential + fun setGoogleIdTokenCredential(credential: GoogleIdTokenCredential?) { + cachedGoogleIdTokenCredential = credential + } override fun getGmailAPIRootUrl() = "https://flowcrypt.test/" override fun isGMailAPIHttpRequestInitializerEnabled(): Boolean = false } diff --git a/FlowCrypt/src/enterpriseUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt b/FlowCrypt/src/enterpriseUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt index 7d19536241..3f390d9187 100644 --- a/FlowCrypt/src/enterpriseUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt +++ b/FlowCrypt/src/enterpriseUiTests/java/com/flowcrypt/email/util/FlavorSettings.kt @@ -7,12 +7,15 @@ package com.flowcrypt.email.util import android.content.Context import androidx.test.espresso.idling.CountingIdlingResource +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import java.util.UUID /** * @author Denys Bondarenko */ object FlavorSettings : EnvironmentSettings { + private var cachedGoogleIdTokenCredential: GoogleIdTokenCredential? = null + private val countingIdlingResource: CountingIdlingResource = CountingIdlingResource( GeneralUtil.genIdlingResourcesName(this::class.java), GeneralUtil.isDebugBuild() @@ -21,6 +24,10 @@ object FlavorSettings : EnvironmentSettings { override fun configure(context: Context) {} override fun getCountingIdlingResource() = countingIdlingResource override fun getGoogleIdToken(): String = UUID.randomUUID().toString() + override fun getGoogleIdTokenCredential(): GoogleIdTokenCredential? = cachedGoogleIdTokenCredential + fun setGoogleIdTokenCredential(credential: GoogleIdTokenCredential?) { + cachedGoogleIdTokenCredential = credential + } override fun getGmailAPIRootUrl() = "https://flowcrypt.test/" override fun isGMailAPIHttpRequestInitializerEnabled(): Boolean = false } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index 0a3216f538..41c8127114 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -21,6 +21,7 @@ import com.flowcrypt.email.api.email.model.AuthCredentials import com.flowcrypt.email.api.email.model.SecurityType import com.flowcrypt.email.api.retrofit.response.model.ClientConfiguration import com.flowcrypt.email.security.KeyStoreCryptoManager +import com.google.android.gms.auth.GoogleAuthUtil import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -109,14 +110,14 @@ data class AccountEntity( useStartTlsForSmtp: Boolean = false, ) : this( email = googleIdTokenCredential.id.lowercase(), - accountType = ACCOUNT_TYPE_GOOGLE, + accountType = GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE, displayName = googleIdTokenCredential.displayName, givenName = googleIdTokenCredential.givenName, familyName = googleIdTokenCredential.familyName, photoUrl = googleIdTokenCredential.profilePictureUri?.toString(), isEnabled = true, isActive = false, - username = requireNotNull(googleIdTokenCredential.id), + username = googleIdTokenCredential.id, password = "", imapServer = GmailConstants.GMAIL_IMAP_SERVER, imapPort = GmailConstants.GMAIL_IMAP_PORT, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt index 08de6cd424..72d6ebbb69 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt @@ -35,7 +35,6 @@ import androidx.core.view.get import androidx.core.view.size import androidx.credentials.ClearCredentialStateRequest import androidx.credentials.CredentialManager -import androidx.credentials.exceptions.ClearCredentialException import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver @@ -92,7 +91,6 @@ import com.flowcrypt.email.util.SharedPreferencesHelper import com.flowcrypt.email.util.exception.CommonConnectionException import com.flowcrypt.email.util.exception.EmptyPassphraseException import kotlinx.coroutines.launch -import java.util.UUID /** * @author Denys Bondarenko @@ -434,18 +432,9 @@ class MainActivity : BaseActivity() { lifecycleScope.launch { activeAccount?.let { accountEntity -> if (accountEntity.accountType == AccountEntity.ACCOUNT_TYPE_GOOGLE) { - try { - CredentialManager.create(this@MainActivity).clearCredentialState( - ClearCredentialStateRequest() - ) - } catch (e: ClearCredentialException) { - e.printStackTraceIfDebugOnly() - showInfoDialog( - requestKey = UUID.randomUUID().toString(), - dialogMsg = e.errorMessage?.toString(), - dialogTitle = getString(R.string.error) - ) - return@launch + runCatching { + CredentialManager.create(this@MainActivity) + .clearCredentialState(ClearCredentialStateRequest()) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 00d7aa2917..01da595c55 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -5,8 +5,11 @@ package com.flowcrypt.email.ui.activity.fragment +import android.accounts.Account import android.app.Activity +import android.content.Context import android.content.Intent +import android.content.MutableContextWrapper import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -15,9 +18,13 @@ import android.widget.Toast import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.exceptions.GetCredentialException import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import com.flowcrypt.email.BuildConfig import com.flowcrypt.email.Constants @@ -36,7 +43,6 @@ import com.flowcrypt.email.extensions.android.os.getParcelableViaExt import com.flowcrypt.email.extensions.android.os.getSerializableViaExt import com.flowcrypt.email.extensions.androidx.fragment.app.countingIdlingResource import com.flowcrypt.email.extensions.androidx.fragment.app.getNavigationResult -import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.setFragmentResultListenerForTwoWayDialog import com.flowcrypt.email.extensions.androidx.fragment.app.showFeedbackFragment @@ -47,12 +53,9 @@ import com.flowcrypt.email.extensions.androidx.navigation.navigateSafe import com.flowcrypt.email.extensions.decrementSafely import com.flowcrypt.email.extensions.exceptionMsg import com.flowcrypt.email.extensions.incrementSafely -import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly -import com.flowcrypt.email.extensions.java.lang.showDialogWithErrorDetails import com.flowcrypt.email.jetpack.viewmodel.CheckCustomerUrlFesServerViewModel import com.flowcrypt.email.jetpack.viewmodel.ClientConfigurationViewModel import com.flowcrypt.email.jetpack.viewmodel.EkmViewModel -import com.flowcrypt.email.jetpack.viewmodel.SignInWithGoogleViewModel import com.flowcrypt.email.model.KeyImportDetails import com.flowcrypt.email.security.model.PgpKeyRingDetails import com.flowcrypt.email.service.CheckClipboardToFindKeyService @@ -60,18 +63,23 @@ import com.flowcrypt.email.ui.activity.fragment.CheckKeysFragment.CheckingState. import com.flowcrypt.email.ui.activity.fragment.CheckKeysFragment.CheckingState.Companion.SKIP_REMAINING_KEYS import com.flowcrypt.email.ui.activity.fragment.base.BaseSingInFragment import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment +import com.flowcrypt.email.util.FlavorSettings import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.exception.AccountAlreadyAddedException import com.flowcrypt.email.util.exception.CommonConnectionException import com.flowcrypt.email.util.exception.EkmNotSupportedException import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.UnsupportedClientConfigurationException -import com.google.android.gms.auth.api.identity.AuthorizationRequest +import com.flowcrypt.email.util.google.GoogleApiClientHelper +import com.google.android.gms.auth.api.identity.AuthorizationClient import com.google.android.gms.auth.api.identity.Identity -import com.google.android.gms.common.api.Scope -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.material.snackbar.Snackbar +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException +import kotlinx.coroutines.launch import org.eclipse.angus.mail.util.MailConnectException import org.pgpainless.util.Passphrase import java.net.HttpURLConnection @@ -85,22 +93,30 @@ class MainSignInFragment : BaseSingInFragment() { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentMainSignInBinding.inflate(inflater, container, false) + private lateinit var credentialManager: CredentialManager + private lateinit var authorizationClient: AuthorizationClient + private var cachedGoogleIdTokenCredential: GoogleIdTokenCredential? = null private var cachedClientConfiguration: ClientConfiguration? = null private var cachedBaseFesUrlPath: String? = null private val checkCustomerUrlFesServerViewModel: CheckCustomerUrlFesServerViewModel by viewModels() private val clientConfigurationViewModel: ClientConfigurationViewModel by viewModels() private val ekmViewModel: EkmViewModel by viewModels() - private val signInWithGoogleViewModel: SignInWithGoogleViewModel by viewModels() private var useStartTlsForSmtp = false - private val forActivityResultSignIn = registerForActivityResult( + private val forActivityResultAuthorization = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() ) { result: ActivityResult -> - if (result.resultCode == Activity.RESULT_OK) { - onUserAuthorizedToGmailApi() + if (result.resultCode == Activity.RESULT_OK && result.data != null) { + try { + authorizationClient.getAuthorizationResultFromIntent(result.data!!) + continueAfterGoogleAuthorization() + } catch (e: ApiException) { + handleGoogleApiException(e) + } } else { - signInWithGoogleViewModel.resetAuthenticationState() + cachedGoogleIdTokenCredential = null + showContent() } } @@ -108,7 +124,7 @@ class MainSignInFragment : BaseSingInFragment() { ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> if (result.resultCode == Activity.RESULT_OK) { - binding?.buttonSignInWithGmail?.callOnClick() + signInWithGmail() } } @@ -121,12 +137,15 @@ class MainSignInFragment : BaseSingInFragment() { override val isDisplayHomeAsUpEnabled: Boolean get() = false - private val cachedGoogleIdTokenCredential: GoogleIdTokenCredential? - get() = signInWithGoogleViewModel.googleIdTokenCredentialStateFlow.value.data + override fun onAttach(context: Context) { + super.onAttach(context) + credentialManager = CredentialManager.create(context) + authorizationClient = Identity.getAuthorizationClient(context) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initViews() + initViews(view) subscribeToCheckAccountSettingsAndSearchBackups() subscribeToCheckPrivateKeys() @@ -138,7 +157,6 @@ class MainSignInFragment : BaseSingInFragment() { initEnterpriseViewModels() initPrivateKeysViewModel() initProtectPrivateKeysLiveData() - initSignInWithGoogleViewModel() } override fun getTempAccount(): AccountEntity? { @@ -175,15 +193,15 @@ class MainSignInFragment : BaseSingInFragment() { handleUnlockedKeys(accountEntity, keys) } - private fun initViews() { - binding?.buttonSignInWithGmail?.setOnClickListener { + private fun initViews(view: View) { + view.findViewById(R.id.buttonSignInWithGmail)?.setOnClickListener { cachedBaseFesUrlPath = null cachedClientConfiguration = null importCandidates.clear() - signInWithGoogleViewModel.authenticateUser(requireActivity()) + signInWithGmail() } - binding?.buttonOtherEmailProvider?.setOnClickListener { + view.findViewById(R.id.buttonOtherEmailProvider)?.setOnClickListener { cachedBaseFesUrlPath = null cachedClientConfiguration = null navController?.navigateSafe( @@ -192,15 +210,15 @@ class MainSignInFragment : BaseSingInFragment() { ) } - binding?.buttonPrivacy?.setOnClickListener { + view.findViewById(R.id.buttonPrivacy)?.setOnClickListener { GeneralUtil.openCustomTab(requireContext(), Constants.FLOWCRYPT_PRIVACY_URL) } - binding?.buttonTerms?.setOnClickListener { + view.findViewById(R.id.buttonTerms)?.setOnClickListener { GeneralUtil.openCustomTab(requireContext(), Constants.FLOWCRYPT_TERMS_URL) } - binding?.buttonSecurity?.setOnClickListener { + view.findViewById(R.id.buttonSecurity)?.setOnClickListener { navController?.navigateSafe( R.id.mainSignInFragment, NavGraphDirections.actionGlobalHtmlViewFromAssetsRawFragment( @@ -210,21 +228,98 @@ class MainSignInFragment : BaseSingInFragment() { ) } - binding?.buttonHelp?.setOnClickListener { + view.findViewById(R.id.buttonHelp)?.setOnClickListener { showFeedbackFragment() } } - private fun handleAuthentication(googleIdTokenCredential: GoogleIdTokenCredential) { - val account = googleIdTokenCredential.id + private fun signInWithGmail() { + cachedGoogleIdTokenCredential = null + FlavorSettings.getGoogleIdTokenCredential()?.let { + cachedGoogleIdTokenCredential = it + continueAfterGoogleAuthorization() + return + } + + viewLifecycleOwner.lifecycleScope.launch { + try { + val response = credentialManager.getCredential( + context = MutableContextWrapper(requireActivity()), + request = GoogleApiClientHelper.generateGoogleSignInRequest() + ) + val credential = response.credential + if (credential is CustomCredential && + credential.type in setOf( + GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL, + GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_SIWG_CREDENTIAL + ) + ) { + cachedGoogleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + authorizeGoogleAccount() + } else { + showContent() + showInfoSnackbar(msgText = getString(R.string.unknown_error)) + } + } catch (_: GetCredentialCancellationException) { + showContent() + } catch (e: GoogleIdTokenParsingException) { + showContent() + showInfoSnackbar(msgText = e.message ?: getString(R.string.unknown_error)) + } catch (e: GetCredentialException) { + showContent() + showInfoSnackbar(msgText = e.errorMessage?.toString() ?: e.type) + } + } + } + + private fun authorizeGoogleAccount() { + val credential = cachedGoogleIdTokenCredential ?: return + val account = Account(credential.id, AccountEntity.ACCOUNT_TYPE_GOOGLE) + authorizationClient.authorize( + GoogleApiClientHelper.generateGoogleAuthorizationRequest(account) + ).addOnSuccessListener { result -> + if (result.hasResolution()) { + val pendingIntent = result.pendingIntent + if (pendingIntent == null) { + showContent() + showInfoSnackbar(msgText = getString(R.string.unknown_error)) + return@addOnSuccessListener + } + forActivityResultAuthorization.launch( + IntentSenderRequest.Builder(pendingIntent.intentSender).build() + ) + } else { + continueAfterGoogleAuthorization() + } + }.addOnFailureListener { exception -> + showContent() + if (exception is ApiException) { + handleGoogleApiException(exception) + } else { + showInfoSnackbar( + msgText = exception.message ?: exception.javaClass.simpleName + ) + } + } + } + + private fun handleGoogleApiException(exception: ApiException) { + showContent() + showInfoSnackbar( + msgText = CommonStatusCodes.getStatusCodeString(exception.statusCode) + ) + } + + private fun continueAfterGoogleAuthorization() { + val credential = cachedGoogleIdTokenCredential ?: return + val account = credential.id cachedBaseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false) val publicEmailDomains = EmailUtil.getPublicEmailDomains() val domain = EmailUtil.getDomain(account) if (domain in publicEmailDomains) { - @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { - signInWithGoogleViewModel.resetAuthenticationState() + cachedGoogleIdTokenCredential = null showInfoDialog( dialogTitle = "", dialogMsg = getString( @@ -235,72 +330,14 @@ class MainSignInFragment : BaseSingInFragment() { isCancelable = true ) } else { - authorizeUserToGmailApi() - } - } else { - authorizeUserToGmailApi() - } - } - - private fun authorizeUserToGmailApi() { - val authorizationRequest: AuthorizationRequest = AuthorizationRequest.builder() - .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))).build() - Identity.getAuthorizationClient(requireContext()) - .authorize(authorizationRequest) - .addOnSuccessListener { authorizationResult -> - if (authorizationResult.hasResolution()) { - // Access needs to be granted by the user. - // At this stage the grant access to Gmail API screen should be displayed - val pendingIntent = authorizationResult.pendingIntent - if (pendingIntent != null) { - try { - forActivityResultSignIn.launch( - IntentSenderRequest.Builder(pendingIntent.intentSender).build() - ) - } catch (e: Exception) { - e.showDialogWithErrorDetails(this) - } - } else { - showInfoDialog( - dialogTitle = "", - dialogMsg = getString( - R.string.error_occurred_with_details_please_try_again, - "pendingIntent == null" - ) - ) - } - } else { - // Access already granted, continue with user action - onUserAuthorizedToGmailApi() - } - }.addOnFailureListener { exception -> - exception.showDialogWithErrorDetails(this) - } - } - - private fun onUserAuthorizedToGmailApi() { - val account = cachedGoogleIdTokenCredential?.id?.lowercase() - val idToken = cachedGoogleIdTokenCredential?.idToken - - if (idToken == null) { - IllegalStateException("idToken == null").showDialogWithErrorDetails(this) - return - } - - if (account != null) { - val publicEmailDomains = EmailUtil.getPublicEmailDomains() - val domain = EmailUtil.getDomain(account) - if (domain in publicEmailDomains) { clientConfigurationViewModel.fetchClientConfiguration( - idToken = idToken, + idToken = credential.idToken, baseFesUrlPath = GeneralUtil.genBaseFesUrlPath(useCustomerFesUrl = false), domain = domain ) - } else { - checkCustomerUrlFesServerViewModel.checkServerAvailability(account) } } else { - IllegalStateException("account == null").showDialogWithErrorDetails(this) + checkCustomerUrlFesServerViewModel.checkServerAvailability(account) } } @@ -525,7 +562,7 @@ class MainSignInFragment : BaseSingInFragment() { } CreateOrImportPrivateKeyDuringSetupFragment.Result.USE_ANOTHER_ACCOUNT -> { - signInWithGoogleViewModel.resetAuthenticationState() + this.cachedGoogleIdTokenCredential = null showContent() } } @@ -657,7 +694,6 @@ class MainSignInFragment : BaseSingInFragment() { } is SSLException -> { - @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { showDialogWithRetryButton(it, REQUEST_CODE_RETRY_CHECK_FES_AVAILABILITY) } else { @@ -680,55 +716,7 @@ class MainSignInFragment : BaseSingInFragment() { } } - private fun initSignInWithGoogleViewModel() { - launchAndRepeatWithViewLifecycle { - signInWithGoogleViewModel.googleIdTokenCredentialStateFlow.collect { - when (it.status) { - Result.Status.SUCCESS -> { - if (it.data != null) { - handleAuthentication(it.data) - } else { - showInfoDialog( - dialogTitle = "", - dialogMsg = getString( - R.string.error_occurred_with_details_please_try_again, - "data == null" - ) - ) - } - signInWithGoogleViewModel.cacheAuthenticationState() - } - - Result.Status.EXCEPTION -> { - when { - it.exception is GetCredentialCancellationException - && "android.credentials.GetCredentialException.TYPE_USER_CANCELED" == it.exception.type - && isConnected() -> { - //do nothing - } - - it.exception is AccountAlreadyAddedException -> { - toast(it.exception.message) - } - - else -> showInfoDialog( - dialogTitle = "", - dialogMsg = it.exceptionMsg - ) - } - - (it.exception as? Exception)?.printStackTraceIfDebugOnly() - signInWithGoogleViewModel.resetAuthenticationState() - } - - else -> {} - } - } - } - } - private fun continueBasedOnFlavorSettings(errorMsg: String) { - @Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == Constants.FLAVOR_NAME_ENTERPRISE) { showDialogWithRetryButton( errorMsg, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/EnvironmentSettings.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/EnvironmentSettings.kt index 7ae47eaf8f..01f0dbf38a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/EnvironmentSettings.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/EnvironmentSettings.kt @@ -7,6 +7,7 @@ package com.flowcrypt.email.util import android.content.Context import androidx.test.espresso.idling.CountingIdlingResource +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import java.util.Properties /** @@ -18,6 +19,7 @@ interface EnvironmentSettings { fun configure(context: Context) fun getCountingIdlingResource(): CountingIdlingResource? = null fun getGoogleIdToken(): String? = null + fun getGoogleIdTokenCredential(): GoogleIdTokenCredential? = null fun getGmailAPIRootUrl(): String? = null fun isGMailAPIHttpRequestInitializerEnabled(): Boolean = true } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt index a5a96307fe..0051b6f502 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt @@ -45,7 +45,7 @@ import com.flowcrypt.email.ui.notifications.ErrorNotificationManager import com.flowcrypt.email.util.exception.CommonConnectionException import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.google.GoogleApiClientHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.GoogleAuthUtil import jakarta.mail.Message import jakarta.mail.MessagingException import jakarta.mail.internet.InternetAddress @@ -496,19 +496,19 @@ class GeneralUtil { accountEntity: AccountEntity ): String = withContext(Dispatchers.IO) { - //before fetch idToken from [GoogleSignInClient] - //we try to get IdToken from the flavor settings + // Before fetching an ID token, try the flavor settings used by UI tests. @Suppress("UNNECESSARY_SAFE_CALL", "KotlinRedundantDiagnosticSuppress") FlavorSettings.getGoogleIdToken()?.let { return@withContext it } - val googleSignInClient = GoogleSignIn.getClient( - context, - GoogleApiClientHelper.generateGoogleSignInOptions(accountEntity.account) - ) - val silentSignIn = googleSignInClient.silentSignIn() - if (!silentSignIn.isSuccessful || silentSignIn.result.isExpired) { + val idToken = try { + GoogleAuthUtil.getToken( + context, + accountEntity.account, + GoogleApiClientHelper.ID_TOKEN_SCOPE + ) + } catch (e: Exception) { if (retryAttempt <= maxRetryAttemptCount) { - //do delay for 10 seconds and try again. Max attempts == maxRetryAttemptCount + // Delay for 10 seconds and try again. Max attempts == maxRetryAttemptCount. delay(TimeUnit.SECONDS.toMillis(10)) return@withContext getGoogleIdTokenSilently( context, @@ -516,7 +516,9 @@ class GeneralUtil { retryAttempt + 1, accountEntity ) - } else throw IllegalStateException("Could not receive idToken") + } else { + throw IllegalStateException("Could not receive idToken", e) + } } val claims = JwtConsumerBuilder() @@ -526,7 +528,7 @@ class GeneralUtil { .setRelaxVerificationKeyValidation() .setSkipSignatureVerification() .build() - .processToClaims(silentSignIn.result.idToken) + .processToClaims(idToken) val email = claims.getClaimValueAsString("email") @@ -534,7 +536,7 @@ class GeneralUtil { throw IllegalStateException("Received tokenId for a wrong account($email)") } - return@withContext requireNotNull(silentSignIn.result.idToken) + return@withContext idToken } suspend fun preProcessException( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/acra/CustomReportSender.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/acra/CustomReportSender.kt index b0ced5ac39..cd4e817b2b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/acra/CustomReportSender.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/acra/CustomReportSender.kt @@ -10,7 +10,7 @@ import android.net.Uri import com.flowcrypt.email.api.retrofit.request.model.CrashReportModel import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.util.google.GoogleApiClientHelper -import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.GoogleAuthUtil import com.google.gson.GsonBuilder import org.acra.ReportField import org.acra.config.CoreConfiguration @@ -89,16 +89,15 @@ class CustomReportSender(config: CoreConfiguration) : HttpSender(config, null, n ) { //add Authorization val finalHeaders = (headers ?: emptyMap()).toMutableMap().apply { - val googleSignInClient = GoogleSignIn.getClient( + val activeAccount = FlowCryptRoomDatabase.getDatabase(context) + .accountDao() + .getActiveAccount() + ?: throw IllegalStateException("Active account is missing") + val idToken = GoogleAuthUtil.getToken( context, - GoogleApiClientHelper.generateGoogleSignInOptions() + activeAccount.account, + GoogleApiClientHelper.ID_TOKEN_SCOPE ) - val silentSignIn = googleSignInClient.silentSignIn() - if (!silentSignIn.isSuccessful || silentSignIn.result.isExpired) { - throw IllegalStateException("Could not receive idToken") - } - - val idToken = silentSignIn.result.idToken put("Authorization", "Bearer $idToken") } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/google/GoogleApiClientHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/google/GoogleApiClientHelper.kt index dd10386c7a..d7f909b160 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/google/GoogleApiClientHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/google/GoogleApiClientHelper.kt @@ -6,9 +6,11 @@ package com.flowcrypt.email.util.google import android.accounts.Account +import androidx.credentials.GetCredentialRequest import com.flowcrypt.email.Constants -import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.auth.api.identity.AuthorizationRequest import com.google.android.gms.common.api.Scope +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption /** * This class describes methods which can be used to work with Google API. @@ -21,15 +23,16 @@ class GoogleApiClientHelper { const val SERVER_CLIENT_ID = "374364070962-n83b6asllhfkhij6slijr61576lqqi3v.apps.googleusercontent.com" - fun generateGoogleSignInOptions(account: Account? = null): GoogleSignInOptions { - val builder = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + const val ID_TOKEN_SCOPE = "audience:server:client_id:$SERVER_CLIENT_ID" - builder.requestScopes(Scope(Constants.SCOPE_MAIL_GOOGLE_COM)) - builder.requestEmail() - builder.requestIdToken(SERVER_CLIENT_ID) - account?.name?.let { builder.setAccountName(it) } + fun generateGoogleSignInRequest(): GetCredentialRequest = GetCredentialRequest( + listOf(GetSignInWithGoogleOption(SERVER_CLIENT_ID)) + ) - return builder.build() - } + fun generateGoogleAuthorizationRequest(account: Account): AuthorizationRequest = + AuthorizationRequest.builder() + .setAccount(account) + .setRequestedScopes(listOf(Scope(Constants.SCOPE_MAIL_GOOGLE_COM))) + .build() } } From 1e68e9a1daa4af4717275202b51c3e32eb1e4a92 Mon Sep 17 00:00:00 2001 From: Denys Bondarenko Date: Thu, 10 Sep 2026 10:54:11 +0300 Subject: [PATCH 22/24] Fix and re-enable UI tests after Credential Manager migration | #2531 --- .../email/ui/AddNewAccountEnterpriseFlowTest.kt | 2 -- .../email/ui/FesDuringSetupCommonFlowTest.kt | 4 ---- .../email/ui/FesDuringSetupConsumerFlowTest.kt | 2 -- .../email/ui/FesDuringSetupEnterpriseFlowTest.kt | 4 ---- .../email/ui/MainSignInFragmentFlowTest.kt | 2 -- .../com/flowcrypt/email/ui/SignInScreenFlowTest.kt | 13 ++++++++----- ...eyToAttesterForImportedKeyDuringSetupFlowTest.kt | 2 -- .../com/flowcrypt/email/ui/base/BaseSignTest.kt | 6 ++++++ ...SignInFragmentEnterpriseTestUseFesUrlFlowTest.kt | 2 -- 9 files changed, 14 insertions(+), 23 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt index 56813bec1a..5c170c78ac 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/AddNewAccountEnterpriseFlowTest.kt @@ -31,7 +31,6 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.not import org.junit.ClassRule -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -45,7 +44,6 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class AddNewAccountEnterpriseFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule() diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt index 2bd9cce835..ebe0a01178 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupCommonFlowTest.kt @@ -23,7 +23,6 @@ import com.flowcrypt.email.util.exception.ApiException import com.google.gson.Gson import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -36,9 +35,6 @@ import java.net.HttpURLConnection */ @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore( - "Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/" -) class FesDuringSetupCommonFlowTest : BaseFesDuringSetupFlowTest() { @get:Rule diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt index 73d4bcc0e8..5600c08c30 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupConsumerFlowTest.kt @@ -28,7 +28,6 @@ import com.flowcrypt.email.util.exception.ApiException import com.google.gson.Gson import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -42,7 +41,6 @@ import java.util.concurrent.TimeUnit */ @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class FesDuringSetupConsumerFlowTest : BaseFesDuringSetupFlowTest() { @get:Rule diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt index 709673ed37..b63bbd4512 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/FesDuringSetupEnterpriseFlowTest.kt @@ -36,7 +36,6 @@ import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.not -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -52,9 +51,6 @@ import java.util.concurrent.TimeUnit @MediumTest @RunWith(AndroidJUnit4::class) @EnterpriseTest -@Ignore( - "Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/" -) class FesDuringSetupEnterpriseFlowTest : BaseFesDuringSetupFlowTest() { @get:Rule var ruleChain: TestRule = RuleChain diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt index 2374ab0c15..bf116e187f 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/MainSignInFragmentFlowTest.kt @@ -50,7 +50,6 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.hamcrest.Matchers.not -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -65,7 +64,6 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class MainSignInFragmentFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule( TestGeneralUtil.genIntentForNavigationComponent( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt index df1382f8e8..c3a5fbbb67 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SignInScreenFlowTest.kt @@ -8,8 +8,6 @@ package com.flowcrypt.email.ui.activity import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.intent.Intents.intended -import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent @@ -25,7 +23,11 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import org.hamcrest.Matchers.allOf -import org.junit.Ignore +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -40,7 +42,6 @@ import org.junit.runner.RunWith @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class SignInScreenFlowTest : BaseTest() { override val activityScenarioRule = activityScenarioRule() @@ -67,7 +68,9 @@ class SignInScreenFlowTest : BaseTest() { .check(matches(isDisplayed())) .perform(click()) //check that the Google Sign-in screen displayed - intended(toPackage("com.google.android.gms")) + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + assertTrue(device.wait(Until.hasObject(By.pkg("com.google.android.gms")), 5000)) + device.pressBack() } @Test diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt index 197b812d5c..a231d7a292 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest.kt @@ -42,7 +42,6 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Before import org.junit.ClassRule -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -59,7 +58,6 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class SubmitPublicKeyToAttesterForImportedKeyDuringSetupFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule( TestGeneralUtil.genIntentForNavigationComponent( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt index 628feff269..26ef47b62a 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseSignTest.kt @@ -22,6 +22,7 @@ import org.jose4j.jws.JsonWebSignature import org.jose4j.jwt.JwtClaims import org.jose4j.jwt.consumer.JwtConsumerBuilder import org.json.JSONObject +import org.junit.After /** @@ -29,6 +30,11 @@ import org.json.JSONObject */ abstract class BaseSignTest : BaseTest() { + @After + fun resetGoogleIdTokenCredential() { + FlavorSettings.setGoogleIdTokenCredential(null) + } + protected fun setupAndClickSignInButton(signInAccountJson: String) { val jsonObject = JSONObject(signInAccountJson) FlavorSettings.setGoogleIdTokenCredential( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt index aa1f8f6d56..3ff680384a 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/fes/login/MainSignInFragmentEnterpriseTestUseFesUrlFlowTest.kt @@ -27,7 +27,6 @@ import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain @@ -41,7 +40,6 @@ import java.net.HttpURLConnection @FlowCryptTestSettings(useIntents = true) @MediumTest @RunWith(AndroidJUnit4::class) -@Ignore("Disabled due to migration to Credential Manager. More details can be found here https://stackoverflow.com/questions/78606467/") class MainSignInFragmentEnterpriseTestUseFesUrlFlowTest : BaseSignTest() { override val activityScenarioRule = activityScenarioRule( TestGeneralUtil.genIntentForNavigationComponent( From a578c6aea2c665b644d08571fd63da1a79f121db Mon Sep 17 00:00:00 2001 From: Denys Bondarenko Date: Thu, 10 Sep 2026 11:07:28 +0300 Subject: [PATCH 23/24] Remove unused SignInWithGoogleViewModel and revert related Result changes --- .../api/retrofit/response/base/Result.kt | 7 +- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 2 +- .../viewmodel/ProcessMessageViewModel.kt | 2 +- .../viewmodel/SignInWithGoogleViewModel.kt | 117 ------------------ .../activity/fragment/MainSignInFragment.kt | 2 +- 5 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt index 372176cb3b..a8fb046d7c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/base/Result.kt @@ -24,17 +24,12 @@ data class Result( val progress: Double? = null ) : Serializable { - fun toCached(): Result { - return copy(status = Status.SUCCESS_CACHED) - } - enum class Status { SUCCESS, ERROR, EXCEPTION, LOADING, - NONE, - SUCCESS_CACHED + NONE } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 519d635380..67f5a6f153 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -265,7 +265,7 @@ class MsgDetailsViewModel( ) } - else -> { + Result.Status.NONE -> { Result.none() } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt index afb4d7e175..69bc565a68 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ProcessMessageViewModel.kt @@ -266,7 +266,7 @@ class ProcessMessageViewModel( ) } - else -> { + Result.Status.NONE -> { Result.none() } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt deleted file mode 100644 index eb47967907..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/SignInWithGoogleViewModel.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.jetpack.viewmodel - -import android.app.Application -import android.content.Context -import androidx.credentials.CredentialManager -import androidx.credentials.CustomCredential -import androidx.credentials.GetCredentialRequest -import androidx.lifecycle.viewModelScope -import com.flowcrypt.email.R -import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.util.coroutines.runners.ControlledRunner -import com.flowcrypt.email.util.exception.AccountAlreadyAddedException -import com.flowcrypt.email.util.google.GoogleApiClientHelper -import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption -import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import org.jose4j.jwt.consumer.JwtConsumerBuilder -import java.util.UUID - -/** - * @author Denys Bondarenko - */ -class SignInWithGoogleViewModel(application: Application) : AccountViewModel(application) { - private val controlledRunnerForGoogleIdTokenCredential = - ControlledRunner>() - private val googleIdTokenCredentialMutableStateFlow: MutableStateFlow> = - MutableStateFlow(Result.none()) - val googleIdTokenCredentialStateFlow: StateFlow> = - googleIdTokenCredentialMutableStateFlow.asStateFlow() - - fun authenticateUser(activityContext: Context) { - viewModelScope.launch { - googleIdTokenCredentialMutableStateFlow.value = Result.loading() - googleIdTokenCredentialMutableStateFlow.value = - controlledRunnerForGoogleIdTokenCredential.cancelPreviousThenRun { - try { - val randomNonce = UUID.randomUUID().toString() - val getSignInWithGoogleOption = - GetSignInWithGoogleOption.Builder(GoogleApiClientHelper.SERVER_CLIENT_ID) - .setNonce(randomNonce) - .build() - - val getCredentialRequest = GetCredentialRequest.Builder() - .addCredentialOption(getSignInWithGoogleOption) - .build() - - val getCredentialResponse = CredentialManager.create(activityContext).getCredential( - context = activityContext, - request = getCredentialRequest - ) - - when (val credential = getCredentialResponse.credential) { - is CustomCredential -> { - if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { - val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) - - //compare nonce from JWT - //mode details cab be found here https://auth0.com/docs/get-started/authentication-and-authorization-flow/implicit-flow-with-form-post/mitigate-replay-attacks-when-using-the-implicit-flow - val idToken = googleIdTokenCredential.idToken - val jwtConsumerBuilder = JwtConsumerBuilder() - //we don't need a verification. Just parse JWT and extract 'nonce' parameter - .setSkipSignatureVerification() - .setExpectedAudience(GoogleApiClientHelper.SERVER_CLIENT_ID) - .build() - val claims = jwtConsumerBuilder.processToClaims(idToken) - if (claims.getClaimValueAsString("nonce") != randomNonce) { - throw IllegalStateException("Security error: 'nonce' mismatch") - } - - //check that the given account is new, not added yet - val existedAccount = roomDatabase.accountDao().getAccountsSuspend().firstOrNull { - it.email.equals(googleIdTokenCredential.id, ignoreCase = true) - } - - if (existedAccount != null) { - throw AccountAlreadyAddedException( - activityContext.getString( - R.string.template_email_already_added, - existedAccount.email - ) - ) - } - - return@cancelPreviousThenRun Result.success(googleIdTokenCredential) - } else { - throw IllegalStateException(activityContext.getString(R.string.unsupported_credentials)) - } - } - - else -> { - throw IllegalStateException(activityContext.getString(R.string.unsupported_credentials)) - } - } - } catch (e: Exception) { - Result.exception(e) - } - } - } - } - - fun resetAuthenticationState() { - googleIdTokenCredentialMutableStateFlow.value = Result.none() - } - - fun cacheAuthenticationState() { - googleIdTokenCredentialMutableStateFlow.value = - googleIdTokenCredentialMutableStateFlow.value.toCached() - } -} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 01da595c55..584d855e50 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -258,7 +258,7 @@ class MainSignInFragment : BaseSingInFragment() { authorizeGoogleAccount() } else { showContent() - showInfoSnackbar(msgText = getString(R.string.unknown_error)) + showInfoSnackbar(msgText = getString(R.string.unsupported_credentials)) } } catch (_: GetCredentialCancellationException) { showContent() From cfa4142208562cc2bec76447e57812e0e6340919 Mon Sep 17 00:00:00 2001 From: Denys Bondarenko Date: Thu, 10 Sep 2026 11:14:09 +0300 Subject: [PATCH 24/24] Fix CredentialManagerMisuse lint error by handling NoCredentialException --- .../email/ui/activity/fragment/MainSignInFragment.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt index 584d855e50..c4d2319ed0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MainSignInFragment.kt @@ -22,6 +22,7 @@ import androidx.credentials.CredentialManager import androidx.credentials.CustomCredential import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope @@ -262,6 +263,9 @@ class MainSignInFragment : BaseSingInFragment() { } } catch (_: GetCredentialCancellationException) { showContent() + } catch (e: NoCredentialException) { + showContent() + showInfoSnackbar(msgText = e.errorMessage?.toString() ?: e.type) } catch (e: GoogleIdTokenParsingException) { showContent() showInfoSnackbar(msgText = e.message ?: getString(R.string.unknown_error))