Skip to content

Offline preflight

gplay preflight inspects a local .aab or .apk and reports what would get your build rejected, or what would break on a user’s device. It makes no API calls and needs no credentials, so it runs on any machine and in any CI job — including the ones that never see a Play service account.

Terminal window
gplay preflight --file app-release.aab

Nine scanners run by default. Every finding is an error, a warning, or info, and --fail-on decides which of those fails the build.

Most tooling that claims to check an Android build matches strings against the compressed archive. That cannot tell android:debuggable="true" apart from android:debuggable="false", and it cannot see an attribute that points at a resource.

preflight decodes AndroidManifest.xml properly — binary AXML for APKs, the aapt2 protobuf encoding for App Bundles — and reads typed attribute values. When a value genuinely cannot be resolved statically (say android:debuggable="@bool/isDebug"), it reports that rather than guessing.

These are the ones worth knowing about, because they fail after upload:

Exported components without android:exported. On targetSdk 31 and above, a component with an <intent-filter> and no explicit android:exported does not install. The app builds fine and Play accepts the upload; the install fails on device.

Foreground service types without their permission. Android 14 requires each android:foregroundServiceType to have a matching permission. A dataSync service without FOREGROUND_SERVICE_DATA_SYNC throws SecurityException when it starts — a crash your CI never sees.

16 KB memory page alignment. Android 15 devices can use 16 KB memory pages, and Play requires native libraries to be aligned for it. preflight reads the real ELF program headers of every .so in the bundle and checks p_align. Misaligned libraries simply fail to load.

Exported providers granting URI permissions. An exported <provider> with grantUriPermissions hands any installed app a path into your data.

Scanner What it checks
manifest debuggable, testOnly, exported components, exported providers, foreground service types, cleartext traffic, allowBackup, package and version sanity
permissions Restricted permissions needing a Play declaration form, sensitive permissions needing a Data safety disclosure, legacy storage on modern targets, duplicates, deprecated permissions
native_libs Missing arm64-v8a, 16 KB page alignment, unstripped debug symbols, extractNativeLibs
metadata Listing text limits and real screenshot pixel dimensions, aspect ratios, counts, icon and feature-graphic sizes. Needs --listings-dir
secrets Private keys, cloud and vendor tokens, service-account JSON, shipped keystores, .git and .env leakage. Scans dex bytecode too
billing Play Billing vs third-party payment processors, BILLING permission with no implementation
privacy Analytics, attribution, and ads SDK inventory, plus AD_ID permission consistency
policy Target API level floor, restricted services, APK-vs-AAB upload format
size Download size budget, dex fragmentation, payload breakdown

List them at any time:

Terminal window
gplay preflight --list-scanners

Point --listings-dir at a Fastlane-style metadata directory and the metadata scanner joins the run. Without it, that scanner is skipped and the report says so.

Terminal window
gplay preflight --file app-release.aab --listings-dir ./fastlane/metadata/android

It validates text limits in runes, not bytes — so an emoji or a CJK character counts once, the way Play counts it — and it decodes each image to check real pixel dimensions rather than trusting the filename.

Terminal window
# Only the ones you care about
gplay preflight --file app.aab --only manifest,permissions,native_libs
# Everything except one
gplay preflight --file app.aab --skip size
# Skip the secret scan on very large builds
gplay preflight --file app.aab --skip-secrets
Terminal window
# Block only on hard blockers
gplay preflight --file app-release.aab --fail-on error
# Stricter
gplay preflight --file app-release.aab --fail-on warning
Exit code Meaning
0 No findings at or above --fail-on
1 Findings at or above --fail-on

GitHub Actions:

- name: Offline preflight
run: |
gplay preflight \
--file app/build/outputs/bundle/release/app-release.aab \
--listings-dir fastlane/metadata/android \
--fail-on error

Because it needs no credentials, this step can run in a fork PR build, before any secret is available.

Terminal window
gplay preflight --file app.aab --output json --pretty

The report carries the detected format, package name, version code and name, min and target SDK, a per-scanner run status, and every finding with its check, severity, message, entry, hint, and — where a Play policy page applies — a ref link.

Terminal window
# Just the blockers
gplay preflight --file app.aab --output json \
| jq '[.findings[] | select(.severity=="error")]'
# Which scanners were skipped, and why
gplay preflight --file app.aab --output json | jq '.scanners[] | select(.skipped)'

If the secrets scanner finds a Google API key (AIza…), it reports a warning, not an error. Android apps embed Maps and Firebase keys by design; they cannot be kept out of the binary. The fix is restricting the key to your package name and signing certificate in Cloud Console.

Credentials that genuinely must never ship — private keys, service-account JSON, sk_live_ secrets, GitHub and Slack tokens, keystores — are errors.

Command Needs auth Scope
gplay preflight No The artifact and listing files, offline
gplay validate Yes Release readiness: local checks plus live track and listing state
gplay checks analyze Yes Google Checks privacy and policy analysis, server-side
gplay release --dry-run Yes The full pipeline against a real edit, discarded before commit

Run them in that order. preflight is the cheapest and catches the most.

Aggressive code shrinking can rename or remove classes, so a missing SDK detection is not proof the SDK is absent — matches are high confidence, misses are not.

The manifest decoder relies on attribute names, which aapt2 emits for normal builds. Deliberately obfuscated APKs that strip names and keep only resource IDs are not fully decoded; preflight falls back to heuristics and says so.

The policy scanner compares targetSdkVersion against a constant that Google raises roughly every August. Pass --min-target-sdk to override it without waiting for a new gplay release.