Mobile development
Google Play Services on Android: availability checks, Google sign-in with Credential Manager and push notifications
5 min read
Google Play Services on Android: availability checks, Google sign-in with Credential Manager and push notifications
Filmoteca, one of the Android apps in my project archive, started as a simple Kotlin app to keep track of films I had watched. During my master's I extended it with two features that depend on Google Play Services: a login screen with a Google account and push notifications through Firebase Cloud Messaging. Neither feature is much code. Almost all the friction was in configuration, and that is what this post is about.
What Play Services actually is
Google Play Services is a system component preinstalled on Android devices that ship with Google Play. It gives apps location, maps, sign-in and many other capabilities without bundling all of that code in the APK. Each API is a separate Gradle artifact, so you only pull in what you use:
dependencies {
implementation("com.google.android.gms:play-services-location:<version>")
// one artifact per API: ads, games, ML Kit, nearby, ...
}
Two practical consequences follow. First, some APIs require extra entries in the manifest: the ads SDK, for example, crashes at startup if its application-id meta-data is missing. Second, Play Services is not guaranteed to exist — devices without Google Play are common in some markets — so the app has to check before relying on it.
Check availability first
The check is short and it is worth putting it at the entry point of any feature that depends on 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
}
The distinction between a resolvable error (outdated or disabled services, which the user can fix) and an unsupported device matters: in the first case you guide the user, in the second you hide the feature and keep the rest of the app working.
The OAuth client and the SHA-1 nobody explains
Google sign-in will not work until Google knows which app is asking. In the Google Cloud console you create an OAuth client ID of type Android with two values: the app's package name and the SHA-1 fingerprint of the certificate that signs the build.
You can read that fingerprint from the keystore or let Gradle print it for every variant:
keytool -list -v -keystore ~/.android/debug.keystore
./gradlew signingReport
Two lessons from getting this wrong:
- Debug and release builds are signed with different keys, so they have different SHA-1s. Register both, or sign-in will work on your machine and fail in the build you actually ship.
- Keystore passwords do not belong in
build.gradle. In the course practice I hardcoded them in the signing config to get it working. Today I keep them outside version control and read them at build time:
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")
}
}
}
From GoogleSignInClient to Credential Manager
The flow I learned first was the classic GoogleSignInClient one: build GoogleSignInOptions asking for profile and email, create the client, check getLastSignedInAccount() in onStart, launch getSignInIntent() with startActivityForResult and read the account in onActivityResult. It works, but it spreads one login across four callbacks, and that API has since been deprecated.
Its replacement is Credential Manager, a single Jetpack API for passwords, passkeys and federated sign-in. With the Google ID library the whole sign-in becomes one suspending call:
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)
}
}
What changed in my head with this migration:
- The Android OAuth client with the SHA-1 is still required; what you pass in code is the web client ID, because the ID token is meant to be verified by your server.
- The app should not trust the profile data it receives on its own. The ID token goes to the backend, which verifies it and creates the session.
- Signing out is
credentialManager.clearCredentialState(ClearCredentialStateRequest()), so the account picker does not silently reuse the previous account.
Push notifications with Firebase Cloud Messaging
FCM is the free channel for sending messages from a server to Android, iOS and web clients. The setup has the same shape as sign-in — configuration first, little code:
- Create a Firebase project and register the app with its package name and debug SHA-1.
- Download
google-services.jsoninto theappmodule and apply thecom.google.gms.google-servicesplugin. - Add the Firebase BoM and the messaging artifact, so library versions stay consistent.
- Declare a
FirebaseMessagingServicein the manifest withandroid:exported="false"and theMESSAGING_EVENTintent filter. - Set a default notification icon, colour and channel through meta-data. Without an icon, Android shows the app icon as a white silhouette.
The part that deserves real attention is the registration token, the address of one app installation:
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
}
}
The token changes when the user reinstalls the app, clears its data, restores it on a new device or when the instance ID is deleted. If the backend only stores the token from the first launch, notifications silently stop arriving for those users. Treat onNewToken as the source of truth and update the server every time it fires. On recent Android versions the app also has to request the notification permission at runtime before anything is shown.
Checklist I now follow
- Check
GoogleApiAvailabilitybefore any Play Services feature and degrade gracefully. - Register the SHA-1 of every signing key you ship with.
- Keep keystore passwords and client secrets out of the repository.
- Use Credential Manager for new sign-in code and verify ID tokens on the server.
- Store FCM tokens per installation and refresh them in
onNewToken.