了解如何在 Android 应用中实现“使用 Google 账号登录”功能

1. 准备工作

了解如何使用 Credential Manager 在 Android 上实现“使用 Google 账号登录”功能。

前提条件

  • Kotlin 进行 Android 开发有基本的了解。
  • 对 Jetpack Compose 有基本的了解(了解详情)。

学习内容

  • 创建 Google Cloud 项目和 OAuth 客户端。
  • 实现底部动作条登录流程。
  • 实现显式按钮登录流程。

所需条件

2. 创建 Android Studio 项目

首先,在 Android Studio 中创建一个新项目:

  1. 打开 Android Studio,然后点击 New Project(新建项目)。Android Studio 欢迎界面
  2. 选择 Phone and Tablet > Empty Activity(手机和平板电脑 > 空 Activity),然后点击 Next(下一步)。Android Studio 项目
  3. 配置项目设置:
    • Name(名称):选择项目名称。
    • Package name(软件包名称):使用默认名称或选择您自己的名称。
    • Minimum SDK(最低 SDK):选择最新的稳定版本或 > Android 14

Android Studio 设置项目

  1. 点击 Finish (完成),然后等待初始项目构建完成。Android Studio 项目构建

3. 设置您的 Google Cloud 项目

创建 Google Cloud 项目

  1. 前往 Google Cloud 控制台,然后选择或创建一个项目。GCP 创建新项目
  2. 依次前往 APIs & Services > OAuth consent screen(API 和服务 > OAuth 权限请求页面)。GCP OAuth 权限请求页面
  3. 点击 Get started (开始),然后填写必填字段:
    • 应用名称:使用 Android 应用的名称。
    • User Support Email(用户支持电子邮件地址):选择您的 Google 账号。
    • Audience(受众群体):选择 External(外部)。
    • Contact Info(联系信息):输入您的电子邮件地址。GCP 应用信息
  4. 查看 Google API 服务:用户数据政策 ,然后点击 Create (创建)。GCP 创建

设置 OAuth 客户端

您需要在 Google Cloud 控制台中同时创建 Web 客户端Android 客户端 ,才能获取用于身份验证的客户端 ID。

  • Android 客户端:通过验证应用的软件包名称和 SHA-1 签名来保护请求。
  • Web 客户端:充当 Google 登录服务的后端客户端。

创建 Android OAuth 2.0 客户端

  1. Clients (客户端)页面上,点击 Create Client (创建客户端),然后选择 Android 作为 Application type (应用类型)。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 fingerprint (SHA-1 指纹)字段中,然后点击 Create (创建)。Android 客户端详细信息

创建 Web OAuth 2.0 客户端

  1. 再次点击 Create Client (创建客户端),然后选择 Web Application (Web 应用)作为 Application type (应用类型)。
  2. 为 Web 客户端命名,将网址/来源字段留空,然后点击 Create(创建)。Web 客户端详细信息
  3. 从确认对话框中复制生成的 Client ID (客户端 ID)。您将在 Kotlin 代码中使用此 ID。复制客户端 ID

4. 设置 Android 虚拟设备

如需测试应用,您可以使用 Android 设备或 Android 虚拟设备 (AVD)。

创建并运行 AVD

  1. 在 Android Studio 中,打开 Device Manager(设备管理器),点击 Create Virtual Device(创建虚拟设备)(或 + 图标),然后选择 Medium Phone(中型手机)。
  2. 选择最新的稳定版本作为系统映像,然后点击 Finish (完成)。
  3. 点击设备旁边的 Play/Run (播放/运行)图标,启动模拟器。正在运行的设备

在设备上登录 Google 账号

  1. 在模拟器上,打开 Settings (设置)应用,然后前往 Google
  2. 点击 Sign in to your Google Account (登录您的 Google 账号),然后按照提示操作。设备已登录

5. 添加依赖项

将身份验证和 Google ID 集成所需的库添加到您的项目中:

  1. 依次前往 File > Project Structure > Dependencies > app (文件 > 项目结构 > 依赖项 > 应用)。
  2. 点击 + > Library Dependency (+ > 库依赖项),搜索 com.google.android.libraries.identity.googleid:googleid,然后选择最新版本(例如 1.1.1)。
  3. 再次点击 + > Library Dependency (+ > 库依赖项),搜索 play-services-auth,然后选择 Group ID 为 com.google.android.gms 的库。
  4. 点击 OK (确定)以应用更改并同步您的项目。已完成的依赖项

6. 实现底部动作条流程

底部动作条流程

底部动作条流程利用 Credential Manager API 为用户提供了一种简化的方式,以便用户 在 Android 上使用 Google 账号登录您的应用。此流程旨在提高速度和便利性,尤其适合回访用户,应在应用启动时触发。

构建登录请求

  1. 首先,打开 MainActivity.kt 并移除默认的 Greeting()GreetingPreview() 函数。
  2. 在第 3 行开始的现有 import 语句之后添加以下 import 语句:
    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):在首次显示可组合项时立即触发登录流程。
  • 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 服务完成初始化之前,底部动作条在应用启动时立即触发时出现竞态条件。
  • 异常处理:捕获并记录常见的凭据错误(例如取消、缺少凭据或令牌解析问题),并使用 Toast 提供用户反馈。

触发底部动作条流程

更新 MainActivity 类,以便在启动时调用 BottomSheet()。将 YOUR_CLIENT_ID_HERE 替换为您的 Web 应用客户端 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. 当应用在模拟器上启动时,应显示登录底部动作条。点击 Continue (继续)以测试流程。底部动作条
  3. 系统应显示一条 Toast 通知,确认登录成功。底部动作条成功

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:与底部动作条流程不同,显式按钮流程使用此选项提示用户选择其 Google 账号,而无需自动过滤。
  • coroutineScope.launch:启动协程,以便在点击按钮时异步执行挂起 signIn 函数。
  • Image:显示品牌化的 siwg_button 可绘制对象,并附加点击监听器以触发流程。

将按钮添加到界面布局

更新 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. 点击 Sign in with Google (使用 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
    }
}