Skip to content

Android

Android checks: Gradle/SDK config, manifest, ProGuard/R8, dependency hygiene, packaging, and store requirements.

51 checks.

android-allow-backup

Severity: Medium

Unscoped backups can copy sensitive app data (auth tokens, databases, shared preferences) to adb or cloud backups, where it is exposed outside the app sandbox.

How to fix: Set android:allowBackup="false" if the app holds sensitive data, or define android:dataExtractionRules (Android 12+) / android:fullBackupContent to exclude secrets from backups.


android-app-consumer-rules-only

Severity: Medium

The build is green and R8 runs — it just runs without these rules, so the classes they were written to keep are stripped or renamed. The breakage shows up only in the minified build, usually as a ClassNotFoundException or a failed reflective lookup on a screen the smoke test never opens, and the stack trace names an obfuscated symbol rather than the rule that is missing.

How to fix: Move the rules into the file the app actually uses — proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' — or add the consumer file to that list explicitly. Keep consumerProguardFiles only in library modules, where AGP packages it into the AAR.


Severity: High

Android verifies a domain only for filters that match the App Links shape exactly. A malformed one is ignored outright, so the link keeps opening the browser or the chooser instead of the app. Nothing reports this: the manifest merges, the build is green, and the defect surfaces as a support ticket about links "not opening the app".

How to fix: Give the autoVerify filter the VIEW action, the DEFAULT and BROWSABLE categories, and <data> with an http/https scheme plus a host. Move any custom scheme (myapp://) into its own separate intent-filter without autoVerify.


android-baseline-profile-missing

Severity: Medium

Without a baseline profile ART has nothing to compile ahead of time, so the first cold start and the first scroll after every install or update run interpreted and JIT-compiled. That is where visible jank concentrates — in the first minutes of a new user's experience. Nothing warns about it: the app is correct, just slower than it needs to be, forever.

How to fix: Apply the androidx.baselineprofile plugin and add a baseline-profile generator module (a Macrobenchmark test that exercises startup and the main scroll), then let it produce src/release/generated/baselineProfiles. Regenerate it when the startup path changes.


android-bundle-language-density-split-disabled

Severity: Medium

Play normally tailors each install to the device: one density bucket of drawables, the languages the user actually has. Disabling a split undoes that for the whole user base — every download carries every locale or every density, and the extra megabytes cost install conversion on exactly the devices and networks that can least afford them.

How to fix: Remove enableSplit = false so Play delivers per-device resources. If the app needs every language present at once (an in-app language picker), keep the language split enabled and pull additional locales on demand with Play Feature Delivery (SplitInstallManager) instead.


android-bundle-missing

Severity: Medium

Android App Bundles (AAB) reduce download size by 15-30% on average through dynamic delivery of resources and native libraries.

How to fix: Replace assembleRelease with bundleRelease in CI, or add a bundleRelease step alongside. Google Play requires AAB for new apps.


android-cleartext-traffic

Severity: Medium

Allowing cleartext traffic exposes the app to man-in-the-middle attacks and data interception.

How to fix: Set android:usesCleartextTraffic="false" or use a network security config to allow cleartext only for specific domains during development.


android-crashlytics-mapping-upload-disabled

Severity: High

R8 renames every class and method, and the mapping file is the only way back. With the upload disabled, Crashlytics receives obfuscated frames it cannot deobfuscate, so the crashes of that release are permanently unreadable. Nothing fails — the build is green, reports keep arriving — and the gap is discovered only when a production stack trace is finally needed.

How to fix: Remove mappingFileUploadEnabled = false (and nativeSymbolUploadEnabled = false) from the release build type — the default uploads the symbols. If it was added to speed builds up, move it to the debug build type instead.


android-debuggable-manifest

Severity: High

A debuggable app exposes internal state, allows attaching debuggers, and bypasses some security restrictions — a serious security risk in production.

How to fix: Remove the android:debuggable attribute from the manifest. Debug mode is controlled by the build type in Gradle.


android-duplicate-async-stack

Severity: Low

Overlapping async stacks (RxJava + Coroutines + AsyncTask) add weight and cognitive load.

How to fix: Consolidate on one concurrency approach (Coroutines/Flow is the modern default).


android-duplicate-image-stack

Severity: Low

Multiple image-loading libraries (Glide, Coil, Picasso, Fresco) duplicate caches and bloat the app.

How to fix: Pick one image-loading library and remove the others.


android-duplicate-serialization-stack

Severity: Low

Multiple JSON/serialization libraries (Gson, Moshi, kotlinx.serialization) add method count, size, and inconsistency.

How to fix: Standardize on a single serialization library across modules.


android-dynamic-dependency-version

Severity: Medium

Dynamic versions can introduce unexpected breaking changes and make builds non-deterministic, complicating debugging and rollbacks.

How to fix: Replace dynamic versions with pinned versions (e.g. "2.10.1" instead of "2.+").


android-edge-to-edge-optout-dead

Severity: Medium

Android documents the level, not a date: for apps targeting API level 36 (Android 16) the attribute is deprecated and disabled, and the app cannot opt out of going edge-to-edge. On a device running Android 16 this theme's opt-out therefore does nothing — the system enforces edge-to-edge regardless, and any layout that assumed the old (non-edge-to-edge) window insets draws under the status bar and navigation bar. The same app running on an Android 15 device is unaffected, so the break stays invisible until it is actually run on 16. Google Play's own target-API requirement (raised to 36 on 2026-08-31) is what keeps pushing modules to this level in the first place.

How to fix: Remove windowOptOutEdgeToEdgeEnforcement — it has no effect at this targetSdk — and handle window insets properly (WindowInsetsCompat, Scaffold contentWindowInsets, SafeArea, depending on the UI toolkit) instead of relying on the legacy non-edge-to-edge layout.


android-excessive-permissions

Severity: Low

Excessive permissions erode user trust, may trigger additional Play Store review, and increase the attack surface.

How to fix: Use tools:node="remove" in the app manifest to strip SDK-contributed permissions you do not need, and verify the final merged set with aapt dump permissions app.apk.


android-flutter-uncompressed-native-libs

Severity: Medium

Since Android 6.0+ the system can load .so files directly from the APK. Extracting them wastes storage and increases install size.

How to fix: Remove android:extractNativeLibs="true" from AndroidManifest.xml or set it to false. Ensure minSdk >= 23 and .so files are page-aligned (default with AGP 3.6+).


android-foreground-service-permission-drift

Severity: High

On Android 14 and above, starting a typed foreground service without both FOREGROUND_SERVICE and the type-specific permission throws SecurityException at runtime. Nothing catches it earlier: the manifest merges, the build succeeds, and the app installs. The crash lands the first time that particular service starts on a 14+ device — often behind a feature that only some users reach.

How to fix: Add the matching <uses-permission> for every declared foregroundServiceType (for example android.permission.FOREGROUND_SERVICE_LOCATION for type "location"), alongside android.permission.FOREGROUND_SERVICE. If the service does not need the type, drop the attribute or switch it to "shortService".


android-gradle-overview

Severity: Info

Understanding the Gradle configuration helps identify SDK targeting issues and build setup problems.

How to fix: Review SDK versions to ensure they align with your target audience and platform requirements.


android-hardcoded-signing

Severity: High

Committing signing credentials leaks release-signing material into version control. Anyone with repo access (or git history) can sign builds that impersonate your app.

How to fix: Move signing credentials to a git-ignored keystore.properties file or CI environment variables, and read them via System.getenv("...") or project.findProperty("...") in signingConfigs.


android-high-risk-restricted-permissions

Severity: Medium

Play-restricted permissions (SMS/Call-Log, QUERY_ALL_PACKAGES, MANAGE_EXTERNAL_STORAGE, AccessibilityService, …) require a Permissions Declaration and a core-feature justification. Shipping one the app does not genuinely need risks app removal — and this is stricter than the count-based excessive-permissions heuristic.

How to fix: Remove any of these your app does not require as a core feature. For the rest, file the Play Console Permissions Declaration and keep the justification current.


android-implied-feature-undeclared

Severity: Medium

Play infers a required hardware feature from certain permissions (camera and camera autofocus, microphone, location, Bluetooth, telephony, Wi-Fi) and filters store visibility to devices that have it, unless the app declares the <uses-feature> itself — required="true" if it genuinely needs the hardware, required="false" if it degrades gracefully without it. With neither declared, the app silently disappears from tablets, Chromebooks, TVs and Android Auto units that lack the hardware. One permission can imply more than one feature, and Android asks for each by name: "The CAMERA permission implies that autofocus is a required feature. To help ensure proper filtering on Google Play when your app manifest includes the CAMERA permission, explicitly specify that your app uses the autofocus feature and indicate whether it is required or not." Declaring only android.hardware.camera therefore still leaves autofocus inferred as required, and declaring android.hardware.camera.any does not answer for android.hardware.camera at all. The build stays green and the APK is valid; the gap only shows up months later, in an install graph.

How to fix: Add a <uses-feature android:name="…" android:required="false"/> for each implied feature the app can run without (or required="true" for one it genuinely needs), naming exactly the feature in the evidence line — one per line, since a single permission can imply several. android.hardware.camera.any does not answer for android.hardware.camera: Android's Chromebook guidance asks for camera.any, but "the CAMERA permission implies that your app also uses android.hardware.camera. A back camera is a required feature unless android.hardware.camera is declared with android:required='false'" — so declare both.


android-incomplete-density-buckets

Severity: Low

A bitmap supplied for a single density is up/down-scaled by the OS on all other devices, causing blurry or jagged rendering. Providing the right buckets (or a vector) keeps assets crisp.

How to fix: Provide the asset across the standard density buckets (mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi), or convert it to a vector drawable (VectorDrawable) which scales without per-density variants.


android-insecure-exported-component

Severity: Medium

Exported components without a guarding permission can be invoked by any other app on the device, enabling component-injection, unauthorized access to internal functionality, and data leakage.

How to fix: Set android:exported="false" on any component no other app needs to start — that is the fix for most of these. Only where a component must stay reachable by a known caller, gate it with a custom <permission> at android:protectionLevel="signature". Do not add android:permission to a component whose job is public entry: verified App Links, share targets, browsable deep-link and OAuth redirect activities, shortcut creation and widget-configuration activities are excluded from this finding for exactly that reason.


android-jetifier-unneeded

Severity: Low

When the whole graph is already AndroidX, Jetifier runs on every build and produces no useful change — a silent, recurring waste of build time. (Verify no transitive dependency still ships Support Library bytecode before removing it; transitive deps are not visible to static analysis.)

How to fix: Remove android.enableJetifier=true from gradle.properties (run a build first to confirm no dependency still requires it).


android-legacy-external-storage

Severity: Medium

requestLegacyExternalStorage is a temporary opt-out that no longer works on Android 11 (API 30)+. Its presence indicates the app may not handle scoped storage correctly.

How to fix: Migrate file access to the scoped storage APIs (MediaStore, SAF) and remove the requestLegacyExternalStorage attribute.


android-legacy-multidex-unneeded

Severity: Low

Carrying the legacy multidex path at minSdk 21+ adds an unused dependency and build step with no benefit. Removing it trims the dependency graph and clarifies the build config.

How to fix: Remove multiDexEnabled true and the androidx.multidex / com.android.support:multidex dependency — multidex is automatic at minSdk 21+.


android-legacy-support-library

Severity: Medium

The Android Support Library is no longer maintained. AndroidX provides bug fixes, new features, and is required by most modern libraries.

How to fix: Use the Android Studio "Migrate to AndroidX" refactoring tool to replace com.android.support dependencies with their AndroidX equivalents.


android-lint-checks-disabled

Severity: Low

Lint checks for release builds catch issues like missing translations, unused resources, and accessibility problems before they reach production.

How to fix: Remove "checkReleaseBuilds false" from the lintOptions/lint block to re-enable lint checks for release builds.


android-locale-config-missing

Severity: Medium

The per-app language picker Android 13 added is populated from the locale config. Without one the app is simply absent from Settings > Apps > Language, so a user cannot run it in a language other than the system one — a common need for bilingual users and for anyone whose system language is not their reading language. Nothing in the build reports it; the translations are all there and all unreachable.

How to fix: Add androidResources { generateLocaleConfig = true } to the application module (AGP 8.1+, compileSdk 33+) and set defaultConfig { resourceConfigurations } or localeFilters to the supported set. Do not also commit a hand-written locales_config.xml — with generation on, that is a build error.


android-locale-filters-drift

Severity: Medium

AndroidX and Play Services libraries each ship strings in roughly 80 languages. When the app declares that it supports a handful and never filters, the bundle still carries all of them — megabytes of strings no user of this app will ever read — and the set the app advertises drifts away from the set it actually ships.

How to fix: Set androidResources { localeFilters += [...] } (AGP 8.10+) or defaultConfig { resourceConfigurations += [...] } to exactly the locales the app supports, and keep it in step with the locale config.


android-low-min-sdk

Severity: Low

Supporting very old Android versions increases maintenance burden and limits modern API usage for a negligible share of active devices. This is advisory — Play does not enforce a minimum.

How to fix: Consider raising minSdk to at least 21 to drop legacy support.


android-low-target-sdk

Severity: High–Medium

Google Play blocks uploads (new apps and updates) that target below the required API level — this is a hard publication blocker, not a style issue.

How to fix: Update targetSdk to <required> or higher and address behavioral changes for the new API levels.


android-missing-minify

Severity: Medium

Without code minification, the release APK/AAB contains unused code, increasing bundle size and making reverse engineering easier.

How to fix: Add "minifyEnabled true" (or "isMinifyEnabled = true" in Kotlin DSL) to the release build type and configure ProGuard/R8 rules.


android-missing-proguard

Severity: Medium

Without custom ProGuard rules, R8 may strip classes needed at runtime (e.g., via reflection), causing crashes.

How to fix: Add a proguard-rules.pro file next to the build.gradle with appropriate keep rules for your project.


android-missing-shrink-resources

Severity: Low

Without resource shrinking, unused resources remain in the APK, unnecessarily increasing bundle size.

How to fix: Add "shrinkResources true" (or "isShrinkResources = true" in Kotlin DSL) to the release build type. Note: this requires minifyEnabled to be true.


android-mixed-kapt-ksp

Severity: Medium

Running both kapt and ksp in the same module doubles annotation processing work, significantly slowing incremental builds.

How to fix: Migrate all annotation processors in these modules from kapt to ksp where ksp plugins are available.


android-native-lib-16kb-alignment

Severity: High

Android 15 devices can use a 16 KB memory page size, and Google Play requires new releases targeting Android 15+ to support them. A 4 KB-aligned library fails to load there — the app crashes the moment that code path runs — and the release is rejected at upload. Nothing in the build reports it: the alignment lives inside the binary, so a current NDK and a current AGP happily package a prebuilt library that was linked years ago.

How to fix: Rebuild first-party libraries with NDK r27+ (or link with -Wl,-z,max-page-size=16384) and update AGP to 8.5.1+ so the packaged libraries stay aligned. For a prebuilt dependency, upgrade to a release that ships 16 KB-aligned binaries — the vendor must rebuild it; re-zipaligning the APK does not fix segment alignment.


android-network-security-config-cleartext

Severity: Medium–Low

A base-config allowing cleartext exposes all app traffic to man-in-the-middle attacks and interception, exactly the risk the manifest flag is meant to close.

How to fix: Set cleartextTrafficPermitted="false" in base-config (or drop the attribute — false is the default on API 28+). Allow cleartext only per-domain via <domain-config> for local dev hosts.


android-network-security-user-ca-trusted

Severity: High–Medium

From API 24 on, Android stopped trusting user-installed CAs by default precisely because any certificate a proxy, MDM profile or piece of malware can install then transparently decrypts and rewrites the app's HTTPS traffic. A base-config that trusts "user" hands that back — silently, with nothing broken and nothing logged.

How to fix: Drop the user certificate source from base-config (trust the system store only), or scope it to a <domain-config> that only matches an internal/QA endpoint that genuinely needs a proxy.


android-non-transitive-r-class

Severity: Low

Transitive R classes copy every dependency's resource IDs into every module's R class — larger DEX and more R-class recompilation on every incremental build. Non-transitive R (the AGP 8.0+ default) gives each module only its own IDs, trimming size and speeding builds.

How to fix: Add android.nonTransitiveRClass=true to the root gradle.properties and run the AGP "Refactor → Migrate to Non-Transitive R Classes" tool to fix any newly-qualified R references.


android-nonfinal-resids-disabled

Severity: Low

Non-final R IDs let AGP skip re-generating and re-compiling R classes for unchanged library modules, speeding incremental builds. Explicitly disabling it silently gives that up.

How to fix: Remove android.nonFinalResIds=false (or set it to true) to use non-final R-class IDs.


android-oversized-resource

Severity: Varies with what is found

Large resource files bloat the APK/AAB, increase download time, and consume more device storage.

How to fix: Compress the resource, convert images to WebP, or use vector drawables where possible.


android-proguard-excessive-dontwarn

Severity: Low

Excessive -dontwarn rules suppress warnings that could indicate missing classes or broken dependencies at runtime.

How to fix: Cross-reference suppressed warnings with actual dependencies via ./gradlew :app:dependencies and remove stale -dontwarn entries. For library-scoped rules, use consumer-rules.pro (AGP 4.0+) so each library ships its own ProGuard config.


android-proguard-keep-all

Severity: High

A blanket keep rule negates the benefits of R8/ProGuard — the app remains fully decompilable and the APK size is not reduced.

How to fix: Replace the blanket keep rule with specific keep rules for classes that need to be preserved (e.g., models used with reflection, JNI interfaces).


android-r8-optimization-disabled

Severity: Medium

With optimization off, R8 still shrinks and obfuscates — so the build looks properly configured and the minify check passes — but it never inlines, merges classes, eliminates dead code or devirtualizes calls. The shipped APK is measurably larger and slower than the same setup would otherwise produce, and nothing in the build says so.

How to fix: Switch to getDefaultProguardFile("proguard-android-optimize.txt") and remove any -dontoptimize from the project rules. If it was added to work around a library crash, replace it with a targeted -keep for that library and verify on a release build.


android-read-media-user-selected-missing

Severity: Medium

Android 14 lets a user grant only "select photos" instead of "allow all" for these permissions. Without READ_MEDIA_VISUAL_USER_SELECTED, the app cannot tell it only holds partial access, so the system keeps it in a compatibility shim that re-shows the permission/photo-picker dialog on every access instead of remembering the selection. Nothing crashes and no store rejects the build — every user on Android 14+ who picks "select photos" just gets re-prompted forever.

How to fix: Add <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/> to the application module, and handle the case where only partial access was granted (query MediaStore for what was shared, or re-invoke the picker instead of re-requesting the broad permission).


android-repository-confusion-risk

Severity: Medium

JCenter is read-only (since 2021) and Bintray is gone. Keeping them in the repository list is a dependency-confusion vector: the abandoned coordinate namespace can be re-registered and served by an attacker, and builds silently fall back to whichever repo answers first. Nothing fails loudly, so it lingers.

How to fix: Remove jcenter()/Bintray from the repositories block. Resolve every dependency from mavenCentral() / google() (or your own trusted repo), and pin sources so coordinates cannot silently move between repositories.


android-snapshot-dependency

Severity: High

SNAPSHOT versions can change at any time without notice, causing unpredictable build failures and runtime issues in production.

How to fix: Replace SNAPSHOT versions with stable release versions.


android-split-abi-missing

Severity: Medium

Without ABI splits, the APK includes native libraries for all architectures, increasing download size by 2-4x for each user.

How to fix: Add ABI splits in build.gradle or migrate to Android App Bundle (AAB) which handles this automatically.


android-unsafe-tls-validation

Severity: High

An empty checkServerTrusted or a HostnameVerifier that returns true turns off certificate/hostname validation — every proxy or attacker on the path can transparently MITM the connection and read/modify traffic. The build succeeds and the app works, so the hole is invisible in normal testing.

How to fix: Remove the custom TrustManager / HostnameVerifier and use the platform defaults. If you must pin a self-signed or private-CA cert, use a proper certificate-pinning setup (e.g. OkHttp CertificatePinner or a network-security-config trust anchor) instead of trusting everything.


android-webview-debug-flags

Severity: Medium

setWebContentsDebuggingEnabled(true) leaves production WebViews open to remote DOM inspection; setAllowUniversalAccessFromFileURLs/setAllowFileAccessFromFileURLs(true) let file:// pages read any origin or local file — a data-exfiltration path. These stay green in the build and are easy to miss.

How to fix: Gate setWebContentsDebuggingEnabled behind BuildConfig.DEBUG, and remove setAllowUniversalAccessFromFileURLs / setAllowFileAccessFromFileURLs (default false) unless a specific, trusted flow requires them.