구글 로그인 회피 로직을 강화한다

승인 계정 우선 조회 후 전체 계정 재시도를 추가한다.
다른 계정 로그인 진입을 위해 구글 전용 옵션 경로를 제공한다.
Android 14 이상에서 Play 서비스 버전을 점검하고 업데이트를 유도한다.
This commit is contained in:
2026-02-10 17:47:46 +09:00
parent 5e43411854
commit d2ab5610c3

View File

@@ -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>(ActivityLoginBinding::inflate) {
@@ -57,6 +63,8 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>(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>(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>(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>(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>(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)