Obtén información para implementar Acceder con Google en tu app para Android

1. Antes de comenzar

En este codelab, aprenderás a implementar el acceso con Google en Android con Credential Manager.

Requisitos previos

  • Conocimientos básicos sobre el uso de Kotlin para el desarrollo de Android
  • Conocimientos básicos de Jetpack Compose (puedes encontrar más información aquí)

Qué aprenderás

  • Cómo crear un proyecto de Google Cloud
  • Cómo crear clientes de OAuth en la consola de Google Cloud
  • Cómo implementar Acceder con Google usando el flujo de la hoja inferior
  • Cómo implementar Acceder con Google usando el flujo del botón

Requisitos

2. Cómo crear un proyecto de Android Studio

Duración: de 3:00 a 5:00

Para comenzar, debemos crear un proyecto nuevo en Android Studio:

  1. Abre Android Studio.
  2. Haz clic en Proyecto nuevo.Bienvenido a Android Studio
  3. Selecciona Phone and Tablet y Empty Activity.Proyecto de Android Studio
  4. Haz clic en Siguiente.
  5. Ahora es momento de configurar algunos elementos del proyecto:
    • Nombre: Es el nombre de tu proyecto.
    • Nombre del paquete: Se completará automáticamente según el nombre de tu proyecto.
    • Save location: De forma predeterminada, debería establecerse en la carpeta en la que Android Studio guarda tus proyectos. Puedes cambiarla a donde quieras.
    • Minimum SDK: Es la versión más antigua del SDK de Android para la que se compiló tu app. En este codelab, usaremos la API 36 (Baklava).
    Proyecto de configuración de Android Studio
  6. Haz clic en Finish.
  7. Android Studio creará el proyecto y descargará las dependencias necesarias para la aplicación base. Esto puede tardar varios minutos. Para ver cómo sucede, haz clic en el ícono de compilación:Compilación de proyectos de Android Studio
  8. Una vez que se complete, Android Studio debería verse similar a lo siguiente:Proyecto de Android Studio compilado

3. Configura el proyecto de Google Cloud

Crea un proyecto de Google Cloud

  1. Ve a la consola de Google Cloud.
  2. Abre tu proyecto o crea uno nuevoCrea un proyecto nuevo de GCPGCP create new project 2GCP create new project 3
  3. Haz clic en APIs y servicios.APIs y servicios de GCP
  4. Ir a la pantalla de consentimiento de OAuthPantalla de consentimiento de OAuth de GCP
  5. Para continuar, deberás completar los campos en Descripción general. Haz clic en Comenzar para comenzar a completar esta información:Botón de Comenzar de GCP
    • Nombre de la app: Es el nombre de esta app, que debe ser el mismo que usaste cuando creaste el proyecto en Android Studio.
    • Correo electrónico de asistencia del usuario: Se mostrará la Cuenta de Google con la que accediste y los Grupos de Google que administras.
    Información de la app de GCP
    • Público:
      • Interna para una app que se usa solo dentro de tu organización Si no tienes una organización asociada al proyecto de Google Cloud, no podrás seleccionar esta opción.
      • Usaremos la opción External.
    Público de GCP
    • Información de contacto: Puede ser cualquier correo electrónico que desees que sea el punto de contacto para la aplicación.
    Información de contacto de GCP
    • Revisa la Política de Datos del Usuario de los Servicios de las APIs de Google.
  6. Una vez que hayas revisado y aceptado la Política de Datos del Usuario, haz clic en Crear.Creación de GCP

Configura clientes de OAuth

Ahora que configuramos un proyecto de Google Cloud, debemos agregar un cliente web y un cliente de Android para poder realizar llamadas a la API al servidor de backend de OAuth con sus IDs de cliente.

Para el cliente web de Android, necesitarás lo siguiente:

  • El nombre del paquete de tu app (p. ej., com.example.example)
  • Firma SHA-1 de tu app
    • ¿Qué es una firma SHA-1?
      • La huella digital SHA-1 es un hash criptográfico que se genera a partir de la clave de firma de tu app. Funciona como un identificador único para el certificado de firma de tu app específica. Piensa en ella como una "firma" digital de tu app.
    • ¿Por qué necesitamos la firma SHA-1?
      • La huella digital SHA-1 garantiza que solo tu app, firmada con tu clave de firma específica, pueda solicitar tokens de acceso con tu ID de cliente de OAuth 2.0, lo que evita que otras apps (incluso aquellas con el mismo nombre de paquete) accedan a los recursos y los datos del usuario de tu proyecto.
      • Piénsalo de esta manera:
        • La clave de firma de tu app es como la llave física de la "puerta" de tu app. Es lo que permite el acceso al funcionamiento interno de la app.
        • La huella digital SHA-1 es como un ID de tarjeta de acceso único que está vinculado a tu llave física. Es un código específico que identifica esa clave en particular.
        • El ID de cliente de OAuth 2.0 es como un código de entrada a un recurso o servicio específico de Google (p.ej., Acceso con Google).
        • Cuando proporcionas la huella digital SHA-1 durante la configuración del cliente de OAuth, básicamente le dices a Google: "Solo la tarjeta llave con este ID específico (SHA-1) puede abrir este código de acceso (ID de cliente)". Esto garantiza que solo tu app pueda acceder a los servicios de Google vinculados a ese código de entrada".

En el caso del cliente web, todo lo que necesitamos es el nombre que deseas usar para identificar al cliente en la consola.

Crea un cliente de OAuth 2.0 para Android

  1. Ve a la página Clientes.Clientes de GCP
  2. Haz clic en Create Client.Crea clientes de GCP
  3. Selecciona Android para el tipo de aplicación.
  4. Deberás especificar el nombre del paquete de tu app.
  5. Desde Android Studio, deberemos obtener la firma SHA-1 de nuestra app y copiarla y pegarla aquí:
    1. Ve a Android Studio y abre el terminal.
    2. Ejecuta este comando:
      keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
      
      Este comando está diseñado para enumerar los detalles de una entrada (alias) específica dentro de un almacén de claves.
      • -list: Esta opción le indica a keytool que muestre el contenido del almacén de claves.
      • -v: Esta opción habilita el resultado detallado, lo que proporciona información más detallada sobre la entrada.
      • -keystore ~/.android/debug.keystore: Especifica la ruta de acceso al archivo del almacén de claves.
      • -alias androiddebugkey: Especifica el alias (nombre de entrada) de la clave que deseas inspeccionar.
      • -storepass android: Proporciona la contraseña del archivo del almacén de claves.
      • -keypass android: Proporciona la contraseña de la clave privada del alias especificado.
    3. Copia el valor de la firma SHA-1:
    Firma de SHA
    1. Regresa a la ventana de Google Cloud y pega el valor de la firma SHA-1:
  6. Tu pantalla ahora debería verse similar a esta, y puedes hacer clic en Crear:Detalles del cliente de AndroidCliente para Android

Crea un cliente web de OAuth 2.0

  1. Para crear un ID de cliente de aplicación web, repite los pasos del 1 al 2 de la sección Crea un cliente de Android y selecciona Aplicación web para el tipo de aplicación.
  2. Asigna un nombre al cliente (este será el cliente de OAuth): Detalles del cliente web
  3. Haz clic en Crear.Clientes en la Web
  4. Copia el ID de cliente de la ventana emergente, ya que lo necesitarás más adelanteCopiar ID de cliente

Ahora que tenemos configurados todos nuestros clientes de OAuth, podemos volver a Android Studio y crear nuestra app para Android de Acceder con Google.

4. Configura un dispositivo virtual de Android

Para probar rápidamente tu aplicación sin un dispositivo Android físico, deberás crear un dispositivo virtual Android en el que puedas compilar y ejecutar tu app de inmediato desde Android Studio. Si quieres realizar pruebas con un dispositivo Android físico, puedes seguir las instrucciones de la documentación para desarrolladores de Android.

Cómo crear un dispositivo virtual de Android

  1. En Android Studio, abre el Administrador de dispositivosAdministrador de dispositivos.
  2. Haz clic en el botón + > Create Virtual DeviceCrea un dispositivo virtual
  3. Desde aquí, puedes agregar cualquier dispositivo que necesites para tu proyecto. Para los fines de este codelab, selecciona Medium Phone y, luego, haz clic en Next.Teléfono mediano
  4. Ahora puedes configurar el dispositivo para tu proyecto asignándole un nombre único, eligiendo la versión de Android que ejecutará el dispositivo y mucho más. Asegúrate de que la API esté configurada en API 36 "Baklava"; Android 16 y, luego, haz clic en Finalizar.Configura el dispositivo virtual
  5. Deberías ver el nuevo dispositivo en el Administrador de dispositivos. Para verificar que el dispositivo se ejecute, haz clic en Ejecutar dispositivo junto al dispositivo que acabas de crearEjecuta el dispositivo 2.
  6. El dispositivo debería estar en funcionamiento.Dispositivo en ejecución

Accede al dispositivo virtual de Android

El dispositivo que acabas de crear funciona. Ahora, para evitar errores cuando pruebes el acceso con Google, deberás acceder al dispositivo con una Cuenta de Google.

  1. Ve a Configuración:
    1. Haz clic en el centro de la pantalla del dispositivo virtual y desliza el dedo hacia arriba.
    Haz clic y desliza
    1. Busca la app de Configuración y haz clic en ella.
    App de Configuración
  2. Haz clic en Google en Configuración.Servicios y preferencias de Google
  3. Haz clic en Acceder y sigue las instrucciones para acceder a tu Cuenta de GoogleAcceso al dispositivo
  1. Ahora deberías haber accedido al dispositivoSe accedió al dispositivo

Tu dispositivo Android virtual ya está listo para la prueba.

5. Cómo agregar dependencias

Duración: 5:00

Para realizar llamadas a la API de OAuth, primero debemos integrar las bibliotecas necesarias que nos permitan realizar solicitudes de autenticación y usar IDs de Google para hacer esas solicitudes:

  • libs.googleid
  • libs.play.services.auth
  1. Ve a File > Project Structure:Estructura del proyecto
  2. Luego, ve a Dependencies > app > "+" > Library Dependency.Dependencias
  3. Ahora debemos agregar nuestras bibliotecas:
    1. En el diálogo de búsqueda, escribe googleid y haz clic en Buscar.
    2. Solo debe haber una entrada. Selecciona la entrada y la versión más alta disponible (en el momento de este codelab, es la versión 1.1.1).
    3. Haz clic en Aceptar.Paquete de ID de Google
    4. Repite los pasos del 1 al 3, pero busca "play-services-auth" y selecciona la línea con "com.google.android.gms" como el ID de grupo y "play-services-auth" como el nombre del artefacto.Autenticación de los Servicios de Play
  4. Haz clic en Aceptar.Dependencias finalizadas

6. Flujo de la hoja inferior

Flujo de la hoja inferior

El flujo de la hoja inferior aprovecha la API de Credential Manager para que los usuarios puedan acceder a tu app con sus cuentas de Google en Android de una manera optimizada. Está diseñada para ser rápida y conveniente, en especial para los usuarios recurrentes. Este flujo debe activarse cuando se inicia la app.

Crea la solicitud de acceso

  1. Para comenzar, quita las funciones Greeting() y GreetingPreview() de MainActivity.kt, ya que no las necesitaremos.
  2. Ahora, debemos asegurarnos de que los paquetes que necesitamos se importen para este proyecto. Agrega las siguientes sentencias import después de las existentes, a partir de la línea 3::
    import android.content.ContentValues.TAG
    import android.content.Context
    import android.credentials.GetCredentialException
    import android.os.Build
    import android.util.Log
    import android.widget.Toast
    import androidx.annotation.RequiresApi
    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
    
  3. A continuación, tenemos que crear nuestra función para generar la solicitud de la hoja inferior. Pega este código debajo de la clase MainActivity.
   //This line is not needed for the project to build, but you will see errors if it is not present.
   //This code will not work on Android versions < UpsideDownCake
   @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)
   }

Desglosemos lo que hace este código:

fun BottomSheet(webClientId: String) {...}: Crea una función llamada BottomSheet que toma un argumento de cadena llamado webClientId.

  • val context = LocalContext.current: Recupera el contexto actual de Android. Esto es necesario para varias operaciones, incluido el lanzamiento de componentes de la IU.
  • LaunchedEffect(Unit) { ... }: LaunchedEffect es un elemento componible de Jetpack Compose que te permite ejecutar una función suspendida (una función que puede pausar y reanudar la ejecución) dentro del ciclo de vida del elemento componible. La unidad como clave significa que este efecto solo se ejecutará una vez cuando se inicie el elemento componible por primera vez.
    • val googleIdOption: GetGoogleIdOption = ...: Crea un objeto GetGoogleIdOption. Este objeto configura el tipo de credencial que se solicita a Google.
      • .Builder(): Se usa un patrón de compilador para configurar las opciones.
      • .setFilterByAuthorizedAccounts(true): Especifica si se permite que el usuario seleccione entre todas las Cuentas de Google o solo las que ya autorizaron la app. En este caso, se establece en verdadero, lo que significa que la solicitud se realizará con las credenciales que el usuario haya autorizado previamente para usar con esta app, si hay alguna disponible.
      • .setServerClientId(webClientId): Establece el ID de cliente del servidor, que es un identificador único para el backend de tu app. Esto es obligatorio para obtener un token de ID.
      • .setNonce(generateSecureRandomNonce()): Establece un nonce, un valor aleatorio, para evitar ataques de reinyección y garantizar que el token de ID esté asociado con la solicitud específica.
      • .build(): Crea el objeto GetGoogleIdOption con la configuración especificada.
    • val request: GetCredentialRequest = ...: Crea un objeto GetCredentialRequest. Este objeto encapsula toda la solicitud de credenciales.
      • .Builder(): Inicia el patrón de compilador para configurar la solicitud.
      • .addCredentialOption(googleIdOption): Agrega el googleIdOption a la solicitud, especificando que queremos solicitar un token de ID de Google.
      • .build(): Crea el objeto GetCredentialRequest.
    • val e = signIn(request, context): Intenta acceder con la solicitud creada y el contexto actual. El resultado de la función signIn se almacena en e. Esta variable contendrá el resultado exitoso o una excepción.
    • if (e is NoCredentialException) { ... }: Esta es una verificación condicional. Si la función signIn falla con una NoCredentialException, significa que no hay cuentas autorizadas disponibles.
      • val googleIdOptionFalse: GetGoogleIdOption = ...: Si la signIn anterior falló, esta parte crea un nuevo GetGoogleIdOption.
      • .setFilterByAuthorizedAccounts(false): Esta es la diferencia fundamental con la primera opción. Inhabilita el filtrado de cuentas autorizadas, lo que significa que se puede usar cualquier Cuenta de Google del dispositivo para acceder.
      • val requestFalse: GetCredentialRequest = ...: Se crea un nuevo GetCredentialRequest con el googleIdOptionFalse.
      • signIn(requestFalse, context): Se intenta acceder con la nueva solicitud que permite usar cualquier cuenta.

En esencia, este código prepara una solicitud a la API de Credential Manager para recuperar un token de ID de Google para el usuario, con los parámetros de configuración proporcionados. Luego, se puede usar GetCredentialRequest para iniciar la IU del administrador de credenciales, en la que el usuario puede seleccionar su Cuenta de Google y otorgar los permisos necesarios.

fun generateSecureRandomNonce(byteLength: Int = 32): String: Define una función llamada generateSecureRandomNonce. Acepta un argumento entero byteLength (con un valor predeterminado de 32) que especifica la longitud deseada del nonce en bytes. Devuelve una cadena, que será la representación codificada en Base64 de los bytes aleatorios.

  • val randomBytes = ByteArray(byteLength): Crea un array de bytes de la longitud de bytes especificada para contener los bytes aleatorios.
  • SecureRandom.getInstanceStrong().nextBytes(randomBytes):
    • SecureRandom.getInstanceStrong(): Esto obtiene un generador de números aleatorios criptográficamente seguro. Esto es fundamental para la seguridad, ya que garantiza que los números generados sean verdaderamente aleatorios y no predecibles. Utiliza la fuente de entropía más potente disponible en el sistema.
    • .nextBytes(randomBytes): Este método completa el array randomBytes con bytes aleatorios generados por la instancia de SecureRandom.
  • return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes):
    • Base64.getUrlEncoder(): Obtiene un codificador Base64 que usa un alfabeto seguro para URLs (con - y _ en lugar de + y /). Esto es importante porque garantiza que la cadena resultante se pueda usar de forma segura en URLs sin necesidad de codificación adicional.
    • .withoutPadding(): Quita los caracteres de relleno de la cadena codificada en Base64. Esto suele ser conveniente para que el nonce sea un poco más corto y compacto.
    • .encodeToString(randomBytes): Codifica randomBytes en una cadena Base64 y la devuelve.

En resumen, esta función genera un nonce aleatorio criptográficamente seguro de una longitud especificada, lo codifica con Base64 seguro para URLs y devuelve la cadena resultante. Esta es una práctica estándar para generar nonces que son seguros para usar en contextos sensibles a la seguridad.

Realiza la solicitud de acceso

Ahora que podemos compilar nuestra solicitud de acceso, podemos usar Credential Manager para acceder. Para ello, debemos crear una función que controle el paso de solicitudes de acceso con Credential Manager y, al mismo tiempo, controle las excepciones comunes que podríamos encontrar.

Para lograrlo, puedes pegar esta función debajo de la función BottomSheet().

//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@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!"
    var e: Exception? = null
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    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())

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")

    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)
        
    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)

    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        return e

    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)

    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
    }
    return e
}

Ahora, analicemos lo que hace el código:

suspend fun signIn(request: GetCredentialRequest, context: Context): Exception?: Define una función suspendida llamada signIn. Esto significa que se puede pausar y reanudar sin bloquear el subproceso principal.Muestra un Exception? que será nulo si el acceso se realiza correctamente o la excepción específica si falla.

Toma dos parámetros:

  • request: Un objeto GetCredentialRequest que contiene la configuración del tipo de credencial que se recuperará (p.ej., ID de Google).
  • context: Es el contexto de Android necesario para interactuar con el sistema.

Para el cuerpo de la función, haz lo siguiente:

  • val credentialManager = CredentialManager.create(context): Crea una instancia de CredentialManager, que es la interfaz principal para interactuar con la API de Credential Manager. Así es como la app iniciará el flujo de acceso.
  • val failureMessage = "Sign in failed!": Define una cadena (failureMessage) que se mostrará en un mensaje emergente cuando falle el acceso.
  • var e: Exception? = null: Esta línea inicializa una variable e para almacenar cualquier excepción que pueda ocurrir durante el proceso, comenzando con nulo.
  • delay(250): Introduce una demora de 250 milisegundos. Esta es una solución alternativa para un posible problema en el que se podría arrojar NoCredentialException inmediatamente cuando se inicia la app, en especial cuando se usa un flujo de BottomSheet. Esto le da tiempo al sistema para inicializar el administrador de credenciales.
  • try { ... } catch (e: Exception) { ... }:Se usa un bloque try-catch para un manejo de errores sólido. Esto garantiza que, si se produce algún error durante el proceso de acceso, la app no falle y pueda controlar la excepción correctamente.
    • val result = credentialManager.getCredential(request = request, context = context): Aquí es donde se realiza la llamada real a la API de Credential Manager y se inicia el proceso de recuperación de credenciales. Toma la solicitud y el contexto como entrada, y le presentará al usuario una IU para que seleccione una credencial. Si la operación se realiza correctamente, se devolverá un resultado que contiene la credencial seleccionada. El resultado de esta operación, GetCredentialResponse, se almacena en la variable result.
    • Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show():Muestra un mensaje breve que indica que el acceso se realizó correctamente.
    • Log.i(TAG, "Sign in Successful!"): Registra un mensaje divertido y exitoso en Logcat.
    • catch (e: GetCredentialException): Controla excepciones de tipo GetCredentialException. Esta es una clase principal para varias excepciones específicas que pueden ocurrir durante el proceso de recuperación de credenciales.
    • catch (e: GoogleIdTokenParsingException): Controla las excepciones que se producen cuando hay un error al analizar el token de ID de Google.
    • catch (e: NoCredentialException): Controla el NoCredentialException, que se arroja cuando no hay credenciales disponibles para el usuario (p.ej., no guardó ninguna o no tiene una Cuenta de Google).
      • Fundamentalmente, esta función devuelve la excepción almacenada en e, NoCredentialException, lo que permite que el llamador controle el caso específico si no hay credenciales disponibles.
    • catch (e: GetCredentialCustomException): Controla las excepciones personalizadas que podría arrojar el proveedor de credenciales.
    • catch (e: GetCredentialCancellationException): Controla el GetCredentialCancellationException, que se arroja cuando el usuario cancela el proceso de acceso.
    • Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show(): Muestra un mensaje emergente que indica que falló el acceso con failureMessage.
    • Log.e(TAG, "", e): Registra la excepción en el logcat de Android con Log.e, que se usa para los errores. Se incluirá el seguimiento de pila de la excepción para ayudar con la depuración. También incluye el emoticón de enojo para divertirte.
  • return e: La función devuelve la excepción si se detectó alguna o null si el acceso se realizó correctamente.

En resumen, este código proporciona una forma de controlar el acceso del usuario con la API de Credential Manager, administra la operación asíncrona, controla los posibles errores y proporciona comentarios al usuario a través de mensajes emergentes y registros, a la vez que agrega un toque de humor al control de errores.

Implementa el flujo de la hoja inferior en la app

Ahora podemos configurar una llamada para activar el flujo de BottomSheet en nuestra clase MainActivity con el siguiente código y nuestro ID de cliente de la aplicación web que copiamos anteriormente de la consola de Google Cloud:

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)
                }
            }
        }
    }
}

Ahora podemos guardar nuestro proyecto (Archivo > Guardar) y ejecutarlo:

  1. Presiona el botón de ejecución:Ejecutar proyecto
  2. Una vez que la app se ejecute en el emulador, deberías ver la ventana emergente de la hoja inferior de acceso. Haz clic en Continuar para probar el accesoHoja inferior
  3. Deberías ver un mensaje Toast que indica que el acceso se realizó correctamente.Hoja inferior de éxito

7. Flujo del botón

GIF del flujo del botón

El flujo de botones para el Acceso con Google facilita a los usuarios registrarse o acceder a tu app para Android con su Cuenta de Google existente. Lo verán si descartan la hoja inferior o si prefieren usar explícitamente su Cuenta de Google para acceder o registrarse. Para los desarrolladores, significa una incorporación más fluida y menos fricción durante el registro.

Si bien esto se puede hacer con un botón de Jetpack Compose listo para usar, utilizaremos un ícono de marca previamente aprobado de la página Lineamientos de desarrollo de la marca de Acceder con Google.

Agrega el ícono de la marca al proyecto

  1. Descarga el archivo ZIP de los íconos de marca aprobados previamente aquí.
  2. Descomprime el archivo signin-assets.zip de tus descargas (esto variará según el sistema operativo de tu computadora). Ahora puedes abrir la carpeta signin-assets y ver los íconos disponibles. En este codelab, usaremos signin-assets/Android/png@2x/neutral/android_neutral_sq_SI@2x.png.
  3. Copia el archivo
  4. Pega en el proyecto en Android Studio en res > drawable haciendo clic con el botón derecho en la carpeta drawable y haciendo clic en Pegar (es posible que debas expandir la carpeta res para verla).Elemento de diseño
  5. Aparecerá un diálogo en el que se te pedirá que cambies el nombre del archivo y confirmes el directorio al que se agregará. Cambia el nombre del activo a siwg_button.png y, luego, haz clic en Aceptar.Botón para agregar

Código del flujo del botón

Este código usará la misma función signIn() que se usa para BottomSheet(), pero usará GetSignInWithGoogleOption en lugar de GetGoogleIdOption, ya que este flujo no aprovecha las credenciales y las llaves de acceso almacenadas en el dispositivo para mostrar las opciones de acceso. Este es el código que puedes pegar debajo de la función 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)
    )
}

Para desglosar lo que hace el código, sigue estos pasos:

fun ButtonUI(webClientId: String): Declara una función llamada ButtonUI que acepta un webClientId (el ID de cliente de tu proyecto de Google Cloud) como argumento.

val context = LocalContext.current: Recupera el contexto actual de Android. Esto es necesario para varias operaciones, incluido el lanzamiento de componentes de la IU.

val coroutineScope = rememberCoroutineScope(): Crea un alcance de corrutina. Se usa para administrar tareas asíncronas, lo que permite que el código se ejecute sin bloquear el subproceso principal. rememberCoroutineScope() es una función de componibilidad de Jetpack Compose que proporcionará un alcance vinculado al ciclo de vida del elemento componible.

val onClick: () -> Unit = { ... }: Esto crea una función lambda que se ejecutará cuando se haga clic en el botón. La función lambda hará lo siguiente:

  • val signInWithGoogleOption: GetSignInWithGoogleOption = GetSignInWithGoogleOption.Builder(serverClientId = webClientId).setNonce(generateSecureRandomNonce()).build(): Esta parte crea un objeto GetSignInWithGoogleOption. Este objeto se usa para especificar los parámetros del proceso de "Acceder con Google". Requiere webClientId y un nonce (una cadena aleatoria que se usa por motivos de seguridad).
  • val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build(): Compila un objeto GetCredentialRequest. Esta solicitud se usará para obtener la credencial del usuario con el Administrador de credenciales. El GetCredentialRequest agrega el GetSignInWithGoogleOption creado anteriormente como una opción para solicitar una credencial de "Acceder con Google".
  • coroutineScope.launch { ... }: Un CoroutineScope para administrar operaciones asíncronas (con corrutinas).
    • signIn(request, context): Llama a nuestra función signIn() definida anteriormente.

Image(...): Esto renderiza una imagen con el painterResource que carga la imagen R.drawable.siwg_button.

  • Modifier.fillMaxSize().clickable(enabled = true, onClick = onClick):
    • fillMaxSize(): Hace que la imagen llene el espacio disponible.
    • clickable(enabled = true, onClick = onClick): Hace que la imagen sea apta para hacer clic y, cuando se hace clic en ella, ejecuta la función lambda onClick definida anteriormente.

En resumen, este código configura un botón de "Acceder con Google" en una IU de Jetpack Compose. Cuando se hace clic en el botón, se prepara una solicitud de credenciales para iniciar Credential Manager y permitir que el usuario acceda con su Cuenta de Google.

Ahora, debes actualizar la clase MainActivity para ejecutar nuestra función 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)
                    }
                }
            }
        }
    }
}

Ahora podemos guardar nuestro proyecto (Archivo > Guardar) y ejecutarlo:

  1. Presiona el botón de ejecución:Ejecutar proyecto
  2. Una vez que la app se ejecute en el emulador, debería aparecer el BottomSheet. Haz clic fuera de ella para cerrarla.Presionar aquí
  3. Ahora deberías ver el botón que creamos en la app. Haz clic en él para ver el diálogo de acceso.Diálogo de acceso
  4. Haz clic en tu cuenta para acceder.

8. Conclusión

¡Terminaste este codelab! Para obtener más información o ayuda sobre Acceder con Google en Android, consulta la sección de preguntas frecuentes a continuación:

Preguntas frecuentes

Código completo de MainActivity.kt

Aquí se muestra el código completo de MainActivity.kt como referencia:

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.credentials.GetCredentialException
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.annotation.RequiresApi
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)
                    }
                }
            }
        }
    }
}

//This line is not needed for the project to build, but you will see errors if it is not present.
//This code will not work on Android versions < UpsideDownCake
@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()

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

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

//This code will not work on Android versions < UPSIDE_DOWN_CAKE when GetCredentialException is
//is thrown.
@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!"
    var e: Exception? = null
    //using delay() here helps prevent NoCredentialException when the BottomSheet Flow is triggered
    //on the initial running of our app
    delay(250)
    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())

        Toast.makeText(context, "Sign in successful!", Toast.LENGTH_SHORT).show()
        Log.i(TAG, "(☞゚ヮ゚)☞  Sign in Successful!  ☜(゚ヮ゚☜)")

    } catch (e: GetCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Failure getting credentials", e)
        
    } catch (e: GoogleIdTokenParsingException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with parsing received GoogleIdToken", e)

    } catch (e: NoCredentialException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": No credentials found", e)
        return e

    } catch (e: GetCredentialCustomException) {
        Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Issue with custom credential request", e)

    } catch (e: GetCredentialCancellationException) {
        Toast.makeText(context, ": Sign-in cancelled", Toast.LENGTH_SHORT).show()
        Log.e(TAG, failureMessage + ": Sign-in was cancelled", e)
    }
    return e
}