Desarrollo móvil
Google Play Services en Android: comprobar disponibilidad, inicio de sesión con Credential Manager y notificaciones
6 min de lectura
Google Play Services en Android: comprobar disponibilidad, inicio de sesión con Credential Manager y notificaciones
Filmoteca, una de las apps Android de mi archivo de proyectos, empezó como una app sencilla en Kotlin para llevar la cuenta de las películas que había visto. Durante el máster la amplié con dos funciones que dependen de Google Play Services: una pantalla de inicio de sesión con cuenta de Google y notificaciones push con Firebase Cloud Messaging. Ninguna de las dos lleva mucho código. Casi toda la fricción estuvo en la configuración, y de eso va este artículo.
Qué es realmente Play Services
Google Play Services es un componente del sistema preinstalado en los dispositivos Android que traen Google Play. Da a las apps ubicación, mapas, inicio de sesión y muchas otras capacidades sin meter todo ese código en el APK. Cada API es un artefacto de Gradle independiente, así que solo incluyes lo que usas:
dependencies {
implementation("com.google.android.gms:play-services-location:<version>")
// one artifact per API: ads, games, ML Kit, nearby, ...
}
De ahí salen dos consecuencias prácticas. La primera, algunas API necesitan entradas adicionales en el manifest: el SDK de anuncios, por ejemplo, se cae al arrancar si falta su meta-data con el id de aplicación. La segunda, no está garantizado que Play Services exista —los dispositivos sin Google Play son habituales en algunos mercados—, así que la app tiene que comprobarlo antes de depender de él.
Primero, comprobar la disponibilidad
La comprobación es corta y merece la pena ponerla en el punto de entrada de cualquier función que dependa de Play Services:
fun ensurePlayServices(activity: Activity): Boolean {
val api = GoogleApiAvailability.getInstance()
val result = api.isGooglePlayServicesAvailable(activity)
if (result == ConnectionResult.SUCCESS) return true
if (api.isUserResolvableError(result)) {
// e.g. outdated Play Services: let the system offer an update
api.getErrorDialog(activity, result, REQUEST_PLAY_SERVICES)?.show()
} else {
Log.w("PlayServices", "This device is not supported")
}
return false
}
Importa distinguir entre un error resoluble (servicios desactualizados o desactivados, que el usuario puede arreglar) y un dispositivo no compatible: en el primer caso guías al usuario; en el segundo ocultas la función y el resto de la app sigue funcionando.
El cliente OAuth y el SHA-1 que nadie explica
El inicio de sesión con Google no funciona hasta que Google sabe qué app lo pide. En la consola de Google Cloud se crea un ID de cliente OAuth de tipo Android con dos datos: el nombre de paquete de la app y la huella SHA-1 del certificado que firma la build.
Esa huella se puede leer del keystore o dejar que Gradle la imprima para cada variante:
keytool -list -v -keystore ~/.android/debug.keystore
./gradlew signingReport
Dos lecciones de haberlo hecho mal:
- Las builds de debug y de release se firman con claves distintas, así que tienen SHA-1 distintos. Registra los dos, o el inicio de sesión funcionará en tu máquina y fallará en la build que publicas.
- Las contraseñas del keystore no van en
build.gradle. En la práctica del curso las dejé escritas en la configuración de firma para que funcionara. Hoy las mantengo fuera del control de versiones y las leo al compilar:
val props = Properties().apply {
rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
}
android {
signingConfigs {
create("release") {
storeFile = file(props.getProperty("signing.storeFile", "release.keystore"))
storePassword = props.getProperty("signing.storePassword")
keyAlias = props.getProperty("signing.keyAlias")
keyPassword = props.getProperty("signing.keyPassword")
}
}
}
De GoogleSignInClient a Credential Manager
El flujo que aprendí primero fue el clásico de GoogleSignInClient: construir unas GoogleSignInOptions que piden perfil y email, crear el cliente, comprobar getLastSignedInAccount() en onStart, lanzar getSignInIntent() con startActivityForResult y leer la cuenta en onActivityResult. Funciona, pero reparte un solo login entre cuatro callbacks, y esa API ya está obsoleta.
Su sustituto es Credential Manager, una única API de Jetpack para contraseñas, passkeys e inicio de sesión federado. Con la librería de Google ID, todo el inicio de sesión se reduce a una llamada suspendida:
val credentialManager = CredentialManager.create(context)
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(WEB_CLIENT_ID) // the web client ID of your backend, not the Android one
.build()
val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
suspend fun signIn(activity: Activity) {
try {
val result = credentialManager.getCredential(activity, request)
val credential = result.credential
if (credential is CustomCredential &&
credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
) {
val google = GoogleIdTokenCredential.createFrom(credential.data)
// send google.idToken to the backend and verify it there
}
} catch (e: GetCredentialException) {
Log.w("Auth", "Sign-in failed or cancelled", e)
}
}
Lo que cambió en mi cabeza con esta migración:
- El cliente OAuth de Android con el SHA-1 sigue siendo necesario; lo que se pasa en el código es el ID de cliente web, porque el token de identidad está pensado para que lo verifique tu servidor.
- La app no debería fiarse por sí sola de los datos de perfil que recibe. El token va al backend, que lo verifica y crea la sesión.
- Cerrar sesión es
credentialManager.clearCredentialState(ClearCredentialStateRequest()), para que el selector de cuentas no reutilice en silencio la cuenta anterior.
Notificaciones push con Firebase Cloud Messaging
FCM es el canal gratuito para enviar mensajes desde un servidor a clientes Android, iOS y web. La configuración tiene la misma forma que el inicio de sesión: primero configuración, poco código.
- Crear un proyecto de Firebase y registrar la app con su nombre de paquete y el SHA-1 de debug.
- Descargar
google-services.jsonen el móduloappy aplicar el plugincom.google.gms.google-services. - Añadir el BoM de Firebase y el artefacto de mensajería, para que las versiones de las librerías sean coherentes.
- Declarar un
FirebaseMessagingServiceen el manifest conandroid:exported="false"y el intent filterMESSAGING_EVENT. - Definir por meta-data el icono, el color y el canal de notificación por defecto. Sin icono, Android muestra el de la app como una silueta blanca.
La parte que merece atención de verdad es el token de registro, la dirección de una instalación concreta de la app:
class PushService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
// send it to the backend, replacing the previous one
}
override fun onMessageReceived(message: RemoteMessage) {
// show a notification or update local state
}
}
El token cambia cuando el usuario reinstala la app, borra sus datos, la restaura en un dispositivo nuevo o cuando se elimina el ID de instancia. Si el backend solo guarda el token del primer arranque, a esos usuarios les dejan de llegar las notificaciones sin que nadie se entere. Trata onNewToken como la fuente de verdad y actualiza el servidor cada vez que se dispare. En las versiones recientes de Android, además, la app tiene que pedir el permiso de notificaciones en tiempo de ejecución antes de mostrar nada.
La lista que sigo ahora
- Comprobar
GoogleApiAvailabilityantes de cualquier función de Play Services y degradar con elegancia. - Registrar el SHA-1 de cada clave con la que firmas lo que publicas.
- Mantener contraseñas de keystore y secretos de cliente fuera del repositorio.
- Usar Credential Manager en el código nuevo de inicio de sesión y verificar los tokens de identidad en el servidor.
- Guardar los tokens de FCM por instalación y renovarlos en
onNewToken.