Differential Revision: https://phabricator.services.mozilla.com/D309482
Android Components > Lib > INTEGRITY-GOOGLEPLAY
Note
Part of Mozilla Android Components. This component is only available on devices that have Google Play Services installed.
An Android Components library that wraps the Google Play Integrity Standard API, exposing it through the concept-integrity interface so calling code stays fully decoupled from Google Play.
Use it to request a signed attestation token that your backend server can verify to confirm:
- The app binary is genuine and unmodified
- The app was installed from Google Play
- The device passes Android's integrity checks
Contents
Background: Why the indirection?
The Play Integrity Standard API is two-phased, and this shapes the entire module structure.
Phase 1 — Prepare
You call StandardIntegrityManager.prepareIntegrityToken() once, passing your Google Cloud project number. Google pre-warms a StandardIntegrityTokenProvider on-device. This involves a network round-trip which
Phase 2 — Request
Once the provider is ready, you call provider.request() as many times as needed. This is cheap because the heavy lifting already happened in Phase 1.
The complication: provider expiry
The pre-warmed provider can expire mid-session. When it does, the API returns error code INTEGRITY_TOKEN_PROVIDER_INVALID. At that point Phase 1 must be re-run before Phase 2 can succeed again.
This two-phase lifecycle, combined with testability requirements and the need to model invalid configuration explicitly, drives the layered interface design rather than a single monolithic class.
IntegrityManagerProvider
└─▶ TokenProviderFactory (runs Phase 1: prepare)
└─▶ TokenProvider (runs Phase 2: request)
└─▶ IntegrityToken
GooglePlayIntegrityClient sits above this chain. It caches the TokenProvider, re-runs Phase 1 transparently if the provider expires, and retries the failed request once before propagating the error.
Flow diagram
Caller
│
request() / warmUp()
│
┌──────────▼──────────┐
│ GooglePlayIntegrity │
│ Client │ caches TokenProvider
└──────────┬──────────┘ retries on expiry
│
┌──────────────┴──────────────┐
│ │
warmUp() / Phase 1 request() / Phase 2
│ │
┌────────▼────────┐ ┌─────────▼────────┐
│ TokenProvider │ │ TokenProvider │
│ Factory │ │ │
└────────┬────────┘ └─────────┬────────┘
│ │
└────────────┬────────────────┘
│
┌─────────────▼──────────────┐
│ Google Play Integrity API │
│ (StandardIntegrityManager) │
│ │
│ prepareIntegrityToken() │
│ requestToken() │
└─────────────┬───────────────┘
│
┌────────▼────────┐
│ IntegrityToken │ signed JWT
│ │ validate server-side
└─────────────────┘
On INTEGRITY_TOKEN_PROVIDER_INVALID:
┌──────────────────────────────────────────┐
│ Refresh: re-run Phase 1, retry Phase 2 │──▶ TokenProviderFactory
└──────────────────────────────────────────┘
Architecture
Key types
| Type | Role |
|---|---|
GooglePlayIntegrityClient |
Top-level IntegrityClient implementation. Orchestrates warm-up, caching, and transparent refresh on provider expiry. |
GoogleProjectNumber |
Sealed class wrapping parse-time validation of the Google Cloud project number from BuildConfig. Models invalid input as Invalid rather than throwing. |
IntegrityManagerProvider |
Interface that creates a StandardIntegrityManager. The default implementation delegates to IntegrityManagerFactory.createStandard(context). |
TokenProviderFactory |
Interface that runs Phase 1 (prepare()). Returns a Result<TokenProvider>. An Invalid project number short-circuits this with InvalidProjectNumber. |
TokenProvider |
Interface that runs Phase 2 (request()). Takes a RequestHashProvider for replay-attack protection. |
RequestHashProvider |
Interface that generates a per-request hash (default: random UUID). |
Provider expiry handling
// Inside GooglePlayIntegrityClient.request()
warmUp()
val provider = checkNotNull(tokenProvider) {
"GooglePlayIntegrityClient is missing a token provider"
}.getOrThrow()
return provider.request(requestHashProvider).onFailure {
if (it.tokenHasExpired) {
refreshTokenProvider()
return request()
}
}
The retry is bounded, it recurses exactly once because a freshly prepared provider will not immediately expire.
Usage
1. Provide your Google Cloud project number
The project number must be configured in your BuildConfig (typically via local.properties or CI secrets. Do not commit it):
// build.gradle
android {
defaultConfig {
buildConfigField("String", "GOOGLE_CLOUD_PROJECT_NUMBER", "\"${project.findProperty('googleCloudProjectNumber') ?: ''}\"")
}
}
Important
You must link a Google Cloud project in the Play Console under Test and release → App integrity → Play Integrity API before tokens will be issued.
2. Construct the client
val integrityClient = GooglePlayIntegrityClient(
tokenProviderFactory = TokenProviderFactory.create(
integrityManagerProvider = IntegrityManagerProvider.create(context),
projectNumber = GoogleProjectNumber.create(BuildConfig.GOOGLE_CLOUD_PROJECT_NUMBER),
),
requestHashProvider = RequestHashProvider.randomHashProvider(),
)
3. Warm up early (recommended)
Google recommends that we warmUp() as early as possible (e.g. on app start or before a user action that will need attestation). This runs Phase 1 in the background so the first request() call is fast.
// In your ViewModel or Application class
lifecycleScope.launch {
integrityClient.warmUp()
}
warmUp() is idempotent — safe to call multiple times.
4. Request a token
lifecycleScope.launch {
val result = runCatching {
val token = integrityClient.request().getOrThrow()
// Send token.value for server-side verification.
// Never inspect or trust the token on the client.
myBackend.verify(token)
}
}
Error handling
request() returns Result<IntegrityToken> and never throws. All failure cases are surfaced as typed exceptions in the Result.failure:
| Exception | When it occurs |
|---|---|
InvalidProjectNumber |
BuildConfig.GOOGLE_CLOUD_PROJECT_NUMBER was empty or not parseable as a Long. Fails at factory creation, before any network call. |
IllegalStateException |
Defensive case, the internal provider was null when request() was called. Should not occur in normal usage. |
StandardIntegrityException |
Propagated from the Play Integrity SDK after a failed Phase 1 or Phase 2 call. The client automatically handles INTEGRITY_TOKEN_PROVIDER_INVALID by refreshing and retrying once. All other error codes are passed through. |
See Play Integrity error codes for the full list of StandardIntegrityException causes.
Testing
The concept-integrity module provides the IntegrityClient interface, allowing you to swap the real implementation for a test double at any layer without touching production code.
Dependencies
| Dependency | Purpose |
|---|---|
concept-integrity |
IntegrityClient and IntegrityToken interface definitions |
com.google.android.play:integrity |
Google Play Integrity SDK |
License
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.