瞭解如何在 Android 應用程式中導入「使用 Google 帳戶登入」功能

1. 事前準備

瞭解如何使用 Credential Manager 在 Android 上實作「使用 Google 帳戶登入」功能。

必要條件

  • 對 Android 開發適用的 Kotlin 有基本瞭解。
  • 具備 Jetpack Compose 的基本知識 (瞭解詳情)。

課程內容

  • 建立 Google Cloud 專案和 OAuth 用戶端。
  • 實作底部功能表登入流程。
  • 實作明確的按鈕登入流程。

需求條件

2. 建立 Android Studio 專案

如要開始使用,請在 Android Studio 中建立新專案:

  1. 開啟 Android Studio,然後按一下「New Project」Android Studio 歡迎畫面
  2. 依序選取「Phone and Tablet」>「Empty Activity」,然後點選「Next」。Android Studio 專案
  3. 設定專案設定:
    • 名稱:選擇專案名稱。
    • 套件名稱:使用預設名稱或選擇自訂名稱。
    • SDK 最低版本:選取最新的穩定版或 > Android 14

Android Studio 設定專案

  1. 按一下「Finish」,然後等待初始專案建構完成。Android Studio 專案建構完成

3. 設定 Google Cloud 專案

建立 Google Cloud 專案

  1. 前往 Google Cloud 控制台,然後選取或建立專案。GCP 建立新專案
  2. 依序前往「APIs & Services」(API 和服務) >「OAuth consent screen」(OAuth 同意畫面)GCP OAuth 同意畫面
  3. 按一下「開始使用」,然後填寫必填欄位:
    • 應用程式名稱:使用 Android 應用程式的名稱。
    • 「使用者支援電子郵件」:選取您的 Google 帳戶。
    • 目標對象:選取「外部」
    • 聯絡資訊:輸入電子郵件地址。GCP 應用程式資訊
  4. 詳閱《Google API 服務:使用者資料政策》,然後按一下「建立」建立 GCP

設定 OAuth 用戶端

您需要在 Google Cloud 控制台中建立網頁用戶端Android 用戶端,才能取得驗證用的用戶端 ID。

  • Android 用戶端:驗證應用程式的套件名稱和 SHA-1 簽章,確保要求安全無虞。
  • 網路用戶端:做為 Google 登入服務的後端用戶端。

建立 Android OAuth 2.0 用戶端

  1. 在「用戶端」頁面中,按一下「建立用戶端」,然後選取「Android」做為「應用程式類型」GCP 建立用戶端
  2. 輸入應用程式的套件名稱 (與 MainActivity.kt 的第 1 行相符)。
  3. 產生 SHA-1 簽章。開啟 Android Studio 終端機並執行下列指令:macOS/Linux:
    keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
    
    Windows:
    keytool -list -v -keystore "C:\Users\USERNAME\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android
    
  4. 從指令輸出內容複製 SHA-1 指紋,貼到控制台的「SHA-1 指紋」欄位,然後按一下「建立」Android 用戶端詳細資料

建立網頁 OAuth 2.0 用戶端

  1. 再次點選「建立用戶端」,然後選取「網頁應用程式」做為「應用程式類型」
  2. 為網路用戶端命名,將「網址/來源」欄位留空,然後按一下「建立」網頁用戶端詳細資料
  3. 從確認對話方塊中複製產生的「用戶端 ID」。您會在 Kotlin 程式碼中使用此 ID。複製用戶端 ID

4. 設定 Android 虛擬裝置

如要測試應用程式,可以使用 Android 實體裝置或 Android 虛擬裝置 (AVD)。

建立並執行 AVD

  1. 在 Android Studio 中開啟「裝置管理工具」,按一下「Create Virtual Device」 (或「+」圖示),然後選取「Medium Phone」
  2. 選取最新的穩定版做為系統映像檔,然後按一下「完成」
  3. 按一下裝置旁邊的「Play/Run」(播放/執行) 圖示,啟動模擬器。執行裝置

在裝置上登入 Google 帳戶

  1. 在模擬器上開啟「設定」應用程式,然後前往「Google」
  2. 按一下「登入 Google 帳戶」,然後按照提示操作。已登入裝置

5. 新增依附元件

將驗證和 Google ID 整合所需的程式庫新增至專案:

  1. 依序前往「File」>「Project Structure」>「Dependencies」>「app」
  2. 依序點選「+」>「程式庫依附元件」,搜尋 com.google.android.libraries.identity.googleid:googleid,然後選取最新版本 (例如 1.1.1)。
  3. 再次按一下「+」>「Library Dependency」,搜尋 play-services-auth,然後選取 Group ID 為 com.google.android.gms 的程式庫。
  4. 按一下「確定」即可套用變更並同步處理專案。已完成的依附元件

6. 實作底部功能表流程

底部功能表流程

底部功能表流程會運用 Credential Manager API,為使用者提供簡化的方式,讓他們在 Android 裝置上使用 Google 帳戶登入應用程式。這個流程專為快速和便利性而設計,特別是針對回訪使用者,應在應用程式啟動時觸發。

建立登入要求

  1. 如要開始使用,請開啟 MainActivity.kt 並移除預設的 Greeting()GreetingPreview() 函式。
  2. 在第 3 行開始的現有匯入陳述式後,新增下列匯入陳述式:
    import android.content.Context
    import android.os.Build
    import android.util.Log
    import android.widget.Toast
    import androidx.annotation.RequiresApi
    import androidx.compose.foundation.Image
    import androidx.compose.foundation.clickable
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Surface
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.res.painterResource
    import androidx.credentials.CredentialManager
    import androidx.credentials.CustomCredential
    import androidx.credentials.GetCredentialRequest
    import androidx.credentials.exceptions.GetCredentialCancellationException
    import androidx.credentials.exceptions.GetCredentialCustomException
    import androidx.credentials.exceptions.GetCredentialException
    import androidx.credentials.exceptions.NoCredentialException
    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.GoogleIdTokenParsingException
    import java.security.SecureRandom
    import java.util.Base64
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.launch
    
    const val TAG = "MainActivity"
    
  3. MainActivity.kt 檔案的 MainActivity 類別下方,新增這個可組合函式:
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Composable
     fun BottomSheet(webClientId: String) {
         val context = LocalContext.current
    
         // LaunchedEffect is used to run a suspend function when the composable is first launched.
         LaunchedEffect(Unit) {
             // Create a Google ID option with filtering by authorized accounts enabled.
             val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
                 .setFilterByAuthorizedAccounts(true)
                 .setServerClientId(webClientId)
                 .setNonce(generateSecureRandomNonce())
                 .build()
    
             // Create a credential request with the Google ID option.
             val request: GetCredentialRequest = GetCredentialRequest.Builder()
                 .addCredentialOption(googleIdOption)
                 .build()
    
             // Attempt to sign in with the created request using an authorized account
             val e = signIn(request, context)
             // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
             // In this case, we attempt to sign in again with filtering disabled.
             if (e is NoCredentialException) {
                 val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                     .setFilterByAuthorizedAccounts(false)
                     .setServerClientId(webClientId)
                     .setNonce(generateSecureRandomNonce())
                     .build()
    
                 val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                     .addCredentialOption(googleIdOptionFalse)
                     .build()
    
                 //We will build out this function in a moment
                 signIn(requestFalse, context)
             }
         }
     }
    
     //This function is used to generate a secure nonce to pass in with our request
     fun generateSecureRandomNonce(byteLength: Int = 32): String {
         val randomBytes = ByteArray(byteLength)
         SecureRandom.getInstanceStrong().nextBytes(randomBytes)
         return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
     }
    

程式碼細目

  • LaunchedEffect(Unit):在首次顯示 Composable 時立即觸發登入流程。
  • GetGoogleIdOption.Builder():設定 Google ID 權杖要求。
    • setFilterByAuthorizedAccounts(true):首先,系統會篩選使用者已授權給這個應用程式的帳戶,嘗試進行無聲登入,盡量減少回訪者的登入阻力。
    • setNonce(...):傳遞 generateSecureRandomNonce() 為每項要求產生的安全隨機 Nonce,防止重送攻擊。
  • signIn(request, context):執行要求。如果失敗並傳回 NoCredentialException (表示沒有先前授權的帳戶),流程會回溯至 setFilterByAuthorizedAccounts(false),讓使用者從裝置上登入的任何 Google 帳戶中選取。

發出登入要求

建構登入要求後,您可以使用 Credential Manager 完成登入程序。建立名為 signIn 的函式,執行要求並處理可能發生的常見例外狀況。

MainActivity.kt 檔案的 BottomSheet 函式下方新增這個函式:

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    return try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        val credential = result.credential
        if (credential is CustomCredential &&
            credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
            val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
            Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
        }

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")
        null
    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
        e
    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        e
    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
        e
    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
        e
    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)
        e
    }
}

程式碼細目

  • credentialManager.getCredential(...):呼叫 Credential Manager API,顯示系統帳戶選取器底部功能表或對話方塊。
  • delay(250):暫停一小段時間,避免在 Credential Manager 服務完成初始化之前,於應用程式啟動時立即觸發底部功能表,導致競爭狀況。
  • 例外狀況處理:擷取並記錄常見的憑證錯誤 (例如取消、缺少憑證或權杖剖析問題),並使用快訊提供使用者意見回饋。

觸發底部功能表流程

更新 MainActivity 類別,在啟動時呼叫 BottomSheet()。將 YOUR_CLIENT_ID_HERE 替換為您的網頁應用程式用戶端 ID:

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    //This will trigger on launch
                    BottomSheet(webClientId)
                }
            }
        }
    }
}

儲存專案 (依序點選「File」>「Save」),然後執行應用程式:

  1. 按下執行按鈕:執行專案
  2. 應用程式在模擬器上啟動時,應該會顯示登入 Bottom Sheet。按一下「繼續」測試流程。底部功能表
  3. 系統應會顯示浮動式訊息通知,確認登入成功。底部功能表成功

7. 實作按鈕流程

按鈕流程 GIF

按鈕流程會提供明確選項,供使用者登入或註冊。使用標準品牌宣傳可確保體驗一致。使用符合「使用 Google 帳戶登入」品牌宣傳指南的預先核准素材資源。

新增品牌圖示

  1. 按這裡下載品牌素材資源,然後解壓縮 ZIP 檔案。
  2. 複製「signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png」。
  3. 在 Android Studio 中,將檔案貼到「res」>「drawable」資料夾,將檔案重新命名為 siwg_button.png,然後按一下「OK」新增按鈕

按鈕流程程式碼

這個流程會重複使用相同的 signIn 輔助函式,但會傳遞 GetSignInWithGoogleOption,而不是 GetGoogleIdOption。與底部功能表流程不同,明確按鈕流程不會預先篩選或自動提示儲存的憑證或密碼金鑰。將這個可組合函式貼到 BottomSheet 函式下方:

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(signInWithGoogleOption)
            .build()

        coroutineScope.launch {
            signIn(request, context)
        }
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = true, onClick = onClick)
    )
}

程式碼細目

  • GetSignInWithGoogleOption:與 Bottom Sheet 流程不同,明確按鈕流程會使用這個選項提示使用者選取 Google 帳戶,不會自動篩選。
  • coroutineScope.launch:啟動協同程式,在點選按鈕時非同步執行暫停 signIn 函式。
  • Image:顯示品牌 siwg_button 可繪項目,並附加點擊事件監聽器來觸發流程。

將按鈕新增至 UI 版面配置

更新 MainActivity 版面配置,垂直對齊顯示自動 BottomSheet 和明確 ButtonUI 項目:

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

測試按鈕流程

  1. 執行應用程式。
  2. 按一下工作表區域外的位置,即可關閉初始的底部功能表。
  3. 按一下「使用 Google 帳戶登入」按鈕,啟動登入對話方塊,然後選取帳戶。登入對話方塊
  4. 驗證結果:檢查 Logcat,確認使用者名稱/電子郵件地址是否已列印出來。

8. 結語

恭喜!您已成功使用 Android Credential Manager 實作「使用 Google 帳戶登入」功能。

其他資源

完整的 MainActivity.kt 程式碼

以下是 MainActivity.kt 的完整程式碼,供您參考:

package com.example.example

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.example.ui.theme.ExampleTheme
import android.content.ContentValues.TAG
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.credentials.exceptions.GetCredentialException
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialCustomException
import androidx.credentials.exceptions.NoCredentialException
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import java.security.SecureRandom
import java.util.Base64
import kotlinx.coroutines.CoroutineScope
import androidx.compose.runtime.LaunchedEffect
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //replace with your own web client ID from Google Cloud Console
        val webClientId = "YOUR_CLIENT_ID_HERE"

        setContent {
            //ExampleTheme - this is derived from the name of the project not any added library
            //e.g. if this project was named "Testing" it would be generated as TestingTheme
            ExampleTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background,
                ) {
                    Column(
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally

                    ) {
                        //This will trigger on launch
                        BottomSheet(webClientId)

                        //This requires the user to press the button
                        ButtonUI(webClientId)
                    }
                }
            }
        }
    }
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun BottomSheet(webClientId: String) {
    val context = LocalContext.current

    // LaunchedEffect is used to run a suspend function when the composable is first launched.
    LaunchedEffect(Unit) {
        // Create a Google ID option with filtering by authorized accounts enabled.
        val googleIdOption: GetGoogleIdOption = GetGoogleIdOption.Builder()
            .setFilterByAuthorizedAccounts(true)
            .setServerClientId(webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        // Create a credential request with the Google ID option.
        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(googleIdOption)
            .build()

        // Attempt to sign in with the created request using an authorized account
        val e = signIn(request, context)
        // If the sign-in fails with NoCredentialException,  there are no authorized accounts.
        // In this case, we attempt to sign in again with filtering disabled.
        if (e is NoCredentialException) {
            val googleIdOptionFalse: GetGoogleIdOption = GetGoogleIdOption.Builder()
                .setFilterByAuthorizedAccounts(false)
                .setServerClientId(webClientId)
                .setNonce(generateSecureRandomNonce())
                .build()

            val requestFalse: GetCredentialRequest = GetCredentialRequest.Builder()
                .addCredentialOption(googleIdOptionFalse)
                .build()

            signIn(requestFalse, context)
        }
    }
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Composable
fun ButtonUI(webClientId: String) {
    val context = LocalContext.current
    val coroutineScope = rememberCoroutineScope()

    val onClick: () -> Unit = {
        val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption
            .Builder(serverClientId = webClientId)
            .setNonce(generateSecureRandomNonce())
            .build()

        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(signInWithGoogleOption)
            .build()

        coroutineScope.launch {
            signIn(request, context)
        }
    }
    Image(
        painter = painterResource(id = R.drawable.siwg_button),
        contentDescription = "",
        modifier = Modifier
            .fillMaxSize()
            .clickable(onClick = onClick)
    )
}

fun generateSecureRandomNonce(byteLength: Int = 32): String {
    val randomBytes = ByteArray(byteLength)
    SecureRandom.getInstanceStrong().nextBytes(randomBytes)
    return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes)
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
suspend fun signIn(request: GetCredentialRequest, context: Context): Exception? {
    val credentialManager = CredentialManager.create(context)
    val failureMessage = "Sign in failed!"
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    return try {
        // The getCredential is called to request a credential from Credential Manager.
        val result = credentialManager.getCredential(
            request = request,
            context = context,
        )
        Log.i(TAG, result.toString())

        val credential = result.credential
        if (credential is CustomCredential &&
            credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
            val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
            Log.i(TAG, "Signed in as: ${googleIdTokenCredential.id}")
        }

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")
        null
    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)
        e
    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        e
    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, "Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
        e
    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)
        e
    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)
        e
    }
}