diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt index c11fba8f..3a16317f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt @@ -1,6 +1,8 @@ package kr.co.vividnext.sodalive.user.login import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper @@ -13,12 +15,15 @@ import androidx.credentials.Credential import androidx.credentials.CredentialManager import androidx.credentials.CustomCredential import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential.Companion.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL import com.jakewharton.rxbinding4.widget.textChanges @@ -45,6 +50,7 @@ import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity import kr.co.vividnext.sodalive.user.signup.SignUpActivity import org.koin.android.ext.android.inject import java.util.UUID +import androidx.core.net.toUri @OptIn(UnstableApi::class) class LoginActivity : BaseActivity(ActivityLoginBinding::inflate) { @@ -57,6 +63,8 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i private val handler = Handler(Looper.getMainLooper()) private var lineLoginNonce: String? = null + private val minGooglePlayServicesMajor = 24 + private val minGooglePlayServicesMinor = 40 private val lineLoginLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> @@ -127,43 +135,11 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i binding.ivSignUpEmail.setOnClickListener { startSignUp() } binding.ivLoginGoogle.setOnClickListener { - if (!isGoogleLoginAvailable()) { - return@setOnClickListener - } - loadingDialog.show(width = screenWidth) - val credentialManager = CredentialManager.create(this) - - val googleIdOption = GetGoogleIdOption.Builder() - .setServerClientId(BuildConfig.GOOGLE_CLIENT_ID) - .setFilterByAuthorizedAccounts(false) - .setAutoSelectEnabled(false) - .build() - - // Create the Credential Manager request - val request = GetCredentialRequest.Builder() - .addCredentialOption(googleIdOption) - .build() - - lifecycleScope.launch { - try { - // Launch Credential Manager UI - val result = credentialManager.getCredential( - context = this@LoginActivity, - request = request - ) - loadingDialog.dismiss() - - // Extract credential from the result returned by Credential Manager - handleSignIn(result.credential) - } catch (e: GetCredentialException) { - showToast(getString(R.string.login_google_failed)) - Logger.e( - "Couldn't retrieve user's credentials: " + - "${e.javaClass.simpleName}, ${e.localizedMessage}" - ) - loadingDialog.dismiss() - } - } + startGoogleLogin(forceUseAllAccounts = false) + } + binding.ivLoginGoogle.setOnLongClickListener { + startGoogleLogin(forceUseAllAccounts = true) + true } binding.ivLoginKakao.setOnClickListener { loginKakao() } @@ -309,6 +285,83 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i startActivity(nextIntent) } + private fun startGoogleLogin(forceUseAllAccounts: Boolean) { + if (!isGoogleLoginAvailable()) { + return + } + loadingDialog.show(width = screenWidth) + val credentialManager = CredentialManager.create(this) + + lifecycleScope.launch { + try { + val result = if (forceUseAllAccounts) { + getGoogleCredentialWithSignInOption(credentialManager) + } else { + getGoogleCredentialWithAuthorizedFirst(credentialManager) + } + handleSignIn(result.credential) + } catch (e: GetCredentialException) { + showToast(getString(R.string.login_google_failed)) + Logger.e( + "Couldn't retrieve user's credentials: " + + "${e.javaClass.simpleName}, ${e.localizedMessage}" + ) + } finally { + loadingDialog.dismiss() + } + } + } + + private suspend fun getGoogleCredentialWithAuthorizedFirst( + credentialManager: CredentialManager + ): GetCredentialResponse { + return try { + credentialManager.getCredential( + context = this@LoginActivity, + request = buildGoogleIdRequest(filterByAuthorizedAccounts = true) + ) + } catch (e: NoCredentialException) { + Logger.i( + "No authorized account. Retry with all accounts: ${e.localizedMessage}" + ) + credentialManager.getCredential( + context = this@LoginActivity, + request = buildGoogleIdRequest(filterByAuthorizedAccounts = false) + ) + } + } + + private suspend fun getGoogleCredentialWithSignInOption( + credentialManager: CredentialManager + ): GetCredentialResponse { + return credentialManager.getCredential( + context = this@LoginActivity, + request = buildSignInWithGoogleRequest() + ) + } + + private fun buildGoogleIdRequest(filterByAuthorizedAccounts: Boolean): GetCredentialRequest { + val googleIdOption = GetGoogleIdOption.Builder() + .setServerClientId(BuildConfig.GOOGLE_CLIENT_ID) + .setFilterByAuthorizedAccounts(filterByAuthorizedAccounts) + .setAutoSelectEnabled(false) + .build() + + return GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + } + + private fun buildSignInWithGoogleRequest(): GetCredentialRequest { + val signInWithGoogleOption = GetSignInWithGoogleOption.Builder( + BuildConfig.GOOGLE_CLIENT_ID + ).build() + + return GetCredentialRequest.Builder() + .addCredentialOption(signInWithGoogleOption) + .build() + } + private fun isGoogleLoginAvailable(): Boolean { if (BuildConfig.GOOGLE_CLIENT_ID.isBlank()) { Logger.e("Google login blocked: GOOGLE_CLIENT_ID is blank.") @@ -318,6 +371,16 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i val status = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) if (status == ConnectionResult.SUCCESS) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + isGooglePlayServicesVersionOutdated() + ) { + Logger.e( + "Google login blocked: Google Play services outdated for Android 14+." + ) + promptGooglePlayServicesUpdate() + showToast(getString(R.string.login_google_failed)) + return false + } return true } @@ -330,6 +393,46 @@ class LoginActivity : BaseActivity(ActivityLoginBinding::i return false } + private fun isGooglePlayServicesVersionOutdated(): Boolean { + return try { + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageInfo( + "com.google.android.gms", + android.content.pm.PackageManager.PackageInfoFlags.of(0) + ) + } else { + @Suppress("DEPRECATION") + packageManager.getPackageInfo("com.google.android.gms", 0) + } + val versionName = packageInfo.versionName ?: return false + val match = Regex("""^(\d+)\.(\d+)""").find(versionName) ?: return false + val major = match.groupValues[1].toIntOrNull() ?: return false + val minor = match.groupValues[2].toIntOrNull() ?: return false + major < minGooglePlayServicesMajor || + (major == minGooglePlayServicesMajor && minor < minGooglePlayServicesMinor) + } catch (e: Exception) { + Logger.e("Failed to read Google Play services version: ${e.localizedMessage}") + false + } + } + + private fun promptGooglePlayServicesUpdate() { + val marketIntent = Intent( + Intent.ACTION_VIEW, + "market://details?id=com.google.android.gms".toUri() + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val webIntent = Intent( + Intent.ACTION_VIEW, + "https://play.google.com/store/apps/details?id=com.google.android.gms".toUri() + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + try { + startActivity(marketIntent) + } catch (_: Exception) { + startActivity(webIntent) + } + } + private fun navigateToMain() { finishAffinity() val nextIntent = Intent(applicationContext, MainActivity::class.java)