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
- Android Studio (descárgalo aquí)
- Una computadora que cumpla con los requisitos del sistema de Android Studio
- Una computadora que cumpla con los requisitos del sistema de Android Emulator
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:
- Abre Android Studio.
- Haz clic en Proyecto nuevo.
- Selecciona Phone and Tablet y Empty Activity.
- Haz clic en Siguiente.
- 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).
- Haz clic en Finish.
- 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:
- Una vez que se complete, Android Studio debería verse similar a lo siguiente:
3. Configura el proyecto de Google Cloud
Crea un proyecto de Google Cloud
- Ve a la consola de Google Cloud.
- Abre tu proyecto o crea uno nuevo
- Haz clic en APIs y servicios.
- Ir a la pantalla de consentimiento de OAuth
- Para continuar, deberás completar los campos en Descripción general. Haz clic en Comenzar para comenzar a completar esta información:
- 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.
- 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.
- Información de contacto: Puede ser cualquier correo electrónico que desees que sea el punto de contacto para la aplicación.
- Revisa la Política de Datos del Usuario de los Servicios de las APIs de Google.
- Una vez que hayas revisado y aceptado la Política de Datos del Usuario, haz clic en Crear.
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".
- ¿Qué es una firma SHA-1?
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
- Ve a la página Clientes.
- Haz clic en Create Client.
- Selecciona Android para el tipo de aplicación.
- Deberás especificar el nombre del paquete de tu app.
- Desde Android Studio, deberemos obtener la firma SHA-1 de nuestra app y copiarla y pegarla aquí:
- Ve a Android Studio y abre el terminal.
- Ejecuta este comando:
Este comando está diseñado para enumerar los detalles de una entrada (alias) específica dentro de un almacén de claves.keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
-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.
- Copia el valor de la firma SHA-1:
- Regresa a la ventana de Google Cloud y pega el valor de la firma SHA-1:
- Tu pantalla ahora debería verse similar a esta, y puedes hacer clic en Crear:
Crea un cliente web de OAuth 2.0
- 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.
- Asigna un nombre al cliente (este será el cliente de OAuth):
- Haz clic en Crear.
- Copia el ID de cliente de la ventana emergente, ya que lo necesitarás más adelante
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
- En Android Studio, abre el Administrador de dispositivos
.
- Haz clic en el botón + > Create Virtual Device
- 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.
- 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.
- Deberías ver el nuevo dispositivo en el Administrador de dispositivos. Para verificar que el dispositivo se ejecute, haz clic en
junto al dispositivo que acabas de crear
.
- El dispositivo debería estar en funcionamiento.
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.
- Ve a Configuración:
- Haz clic en el centro de la pantalla del dispositivo virtual y desliza el dedo hacia arriba.
- Busca la app de Configuración y haz clic en ella.
- Haz clic en Google en Configuración.
- Haz clic en Acceder y sigue las instrucciones para acceder a tu Cuenta de Google
- Ahora deberías haber accedido 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
- Ve a File > Project Structure:
- Luego, ve a Dependencies > app > "+" > Library Dependency.
- Ahora debemos agregar nuestras bibliotecas:
- En el diálogo de búsqueda, escribe googleid y haz clic en Buscar.
- 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).
- Haz clic en Aceptar.
- 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.
- Haz clic en Aceptar.
6. 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
- Para comenzar, quita las funciones
Greeting()
yGreetingPreview()
deMainActivity.kt
, ya que no las necesitaremos. - 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
- 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 objetoGetGoogleIdOption
. 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 objetoGetGoogleIdOption
con la configuración especificada.
val request: GetCredentialRequest = ...
: Crea un objetoGetCredentialRequest
. 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 objetoGetCredentialRequest
.
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 lasignIn
anterior falló, esta parte crea un nuevoGetGoogleIdOption
..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 nuevoGetCredentialRequest
con elgoogleIdOptionFalse
.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 objetoGetCredentialRequest
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 variableresult
.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 tipoGetCredentialException
. 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 elNoCredentialException
, 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.
- Fundamentalmente, esta función devuelve la excepción almacenada en
catch (e: GetCredentialCustomException)
: Controla las excepciones personalizadas que podría arrojar el proveedor de credenciales.catch (e: GetCredentialCancellationException)
: Controla elGetCredentialCancellationException
, 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:
- Presiona el botón de ejecución:
- 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 acceso
- Deberías ver un mensaje Toast que indica que el acceso se realizó correctamente.
7. 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
- Descarga el archivo ZIP de los íconos de marca aprobados previamente aquí.
- 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
. - Copia el archivo
- 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).
- 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.
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 objetoGetSignInWithGoogleOption
. Este objeto se usa para especificar los parámetros del proceso de "Acceder con Google". RequierewebClientId
y un nonce (una cadena aleatoria que se usa por motivos de seguridad).val request: GetCredentialRequest = GetCredentialRequest.Builder().addCredentialOption(signInWithGoogleOption).build()
: Compila un objetoGetCredentialRequest
. Esta solicitud se usará para obtener la credencial del usuario con el Administrador de credenciales. ElGetCredentialRequest
agrega elGetSignInWithGoogleOption
creado anteriormente como una opción para solicitar una credencial de "Acceder con Google".
coroutineScope.launch { ... }
: UnCoroutineScope
para administrar operaciones asíncronas (con corrutinas).signIn(request, context)
: Llama a nuestra funciónsignIn
() 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:
- Presiona el botón de ejecución:
- Una vez que la app se ejecute en el emulador, debería aparecer el BottomSheet. Haz clic fuera de ella para cerrarla.
- Ahora deberías ver el botón que creamos en la app. Haz clic en él para ver el diálogo de acceso.
- 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
- Stack Overflow
- Guía de solución de problemas del Administrador de credenciales de Android
- Preguntas frecuentes sobre el Administrador de credenciales de Android
- Centro de ayuda para la verificación de apps de OAuth
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
}