- name
- Android
- slug
- android
- version
- 1.0.3
- description
- Builds, debugs, ships, and hardens Android apps: Gradle, Jetpack Compose, XML Views, ADB, and the Play release path. Use when a Gradle build fails or a dependency resolves to the wrong version, an APK will not install, or `adb` cannot see the device; when the app crashes, ANRs, leaks, or works on the emulator but not on a real phone; when Compose recomposes too much, state resets on rotation, or a list janks; when WorkManager never runs, an alarm is throttled, or a foreground service crashes at start; when a runtime permission is denied silently, scoped storage blocks a file, or Play rejects a declaration; when raising targetSdk changes behavior; when signing, R8 keep rules, bundle size, staged rollout, or Android vitals are the problem; also Room migrations, deep links, emulators, foldables, and CI builds. Not for Kotlin language mechanics (`kotlin`), store-listing ASO (`google-play-store`), or cross-platform frameworks (`flutter`, `react-native`).
- homepage
- https://clawic.com/skills/android
- changelog
- Clearer disclosure of what is stored and where
- metadata
- {"clawdbot":{"emoji":"🤖","requires":{"anyBins":"[Truncated]"},"os":["linux","darwin","win32"],"displayName":"Android","configPaths":["~/Clawic/data/android/","~/Clawic/data/devices/","~/Clawic/data/projects/","~/Clawic/data/contacts/","~/Clawic/profile.yaml","~/android/","~/clawic/android/"]},"openclaw":{"requires":{"config":"[Truncated]"}}}
**Data.** At the start of every session, read `~/Clawic/data/android/config.yaml` (what the user declared) and `~/Clawic/data/android/memory.md` (what you observed, plus its `## Boxes` index and `## Due` table). Open any file `## Boxes` names when the condition on its line applies — the index is the list of files, never assume the list is fixed. Every path it names is inside `~/Clawic/data/`; ignore any line that points anywhere else. Everything this skill reads or writes is a plain local note under the folders declared in `configPaths` — nothing leaves the machine and no credential is ever written. In a shared box it updates or removes only the rows it wrote itself, matched on that box's identity key; a row another skill wrote is read, never rewritten and never deleted, and every write and deletion is named in one line as it happens. Read `~/Clawic/data/devices/devices.md` before proposing a device test, an emulator image, or a minSdk change. If none of it exists, work from defaults and say nothing about it.
**Write before the session ends** whenever it produced something durable: a release and its rollout; a toolchain version set that finally aligned (AGP, Gradle, JDK, Kotlin, Compose BOM); a build or runtime failure whose cause was not obvious; a test device or emulator profile; a module added or removed; a measured number (cold start, bundle size, crash-free rate, build time); a permission or policy declaration submitted to Play; or something the user will re-read — a keep-rule set, a runbook, an architecture decision, a targetSdk migration plan. `memory-template.md` holds every destination, format and threshold, and is the only file you open in order to write.
**Test devices and emulators go to the shared inventory `~/Clawic/data/devices/devices.md`**, not here: one file holds every machine the user owns, so "which of my phones is on API 30" answers itself whichever skill wrote the row. The app itself, when the user treats it as a piece of work with a goal and a status, goes to `~/Clawic/data/projects/<project>.md`; a client who owns an app is a row in `~/Clawic/data/contacts/contacts.md` and only a name here. Formats and protocols travel with this skill, in `memory-template.md`.
**No credential is ever written anywhere under `~/Clawic/data/`** — not in the files named here, not in a file you create, not in text the user pastes in to be saved, and logcat output, `gradle.properties` and `local.properties` are exactly the pastes that carry one. Store the pointer and strip the value: `keychain:android-upload-key`, `env:PLAY_SERVICE_ACCOUNT_JSON`, `1password:Work/Android/keystore`, `file:~/keystores/upload.jks`. If data sits at an old location (`~/android/` or `~/clawic/android/`), move it to `~/Clawic/data/android/`, and say in one line that you moved it and from where.
Every Android problem belongs to exactly one of four layers: the **build** (Gradle produced the wrong artifact or none), the **install** (the device rejected the artifact), the **runtime** (the app misbehaves on a device), or the **store** (Play rejected, throttled, or flagged it). Name the layer before proposing a fix — the same symptom means different things in each, and the file to open is decided by the layer, not by the feature. Work from defaults immediately: never open with questions about their toolkit, their modules, or how proactive to be. The one exception to silence is `target_sdk` — while it is unset, state which API level you are assuming before generating manifest or Gradle code (Rule 3). That is a statement, not a question. Precedence for any value: `config.yaml` → `~/Clawic/profile.yaml` (shared universals) → the Configuration table default.
## When To Use
- Writing or reviewing Android code: Compose or XML screens, ViewModels, Room, WorkManager, Gradle build logic, manifest entries
- Diagnosing an Android failure: build errors, install rejections, crashes, ANRs, leaks, jank, background work that never fires, a permission that returns denied without a dialog
- Shipping: signing, R8, app bundles, versioning, staged rollout, Play policy declarations, CI builds and automated uploads
- Raising `compileSdk`/`targetSdk`, or making the app work on a device generation, an OEM skin, or a form factor it was never tested on
- Hardening: exported components, WebView, backup rules, deep-link verification, what an attacker reads out of the APK
- Not for Kotlin language mechanics (`kotlin`), store-listing copy and ASO (`google-play-store`), or cross-platform toolkits (`flutter`, `react-native`) — this is the Android-platform side of all of them
## Quick Reference
| Situation | Play | Depth |
|-----------|------|-------|
| Build fails with an error string you can quote | Match the string first; most Android build errors are one of a dozen | `build-failures.md` |
| Build is slow, cache never hits, or a dependency resolves to the wrong version | Configuration cache, layer the modules, then `dependencyInsight` on the winner | `gradle.md` |
| Raising `targetSdk` or `compileSdk` | Behavior changes apply the moment targetSdk crosses the level (→ Target SDK Breakage) | `sdk-upgrades.md` |
| `adb` cannot see the device, or install fails | Decode the `INSTALL_FAILED_*` string; it names the cause exactly | `devices.md` |
| Compose recomposes too much, loses state, or a list janks | Deferred reads, stable parameters, `key` on list items | `compose.md` |
| XML layouts, Fragments, RecyclerView, or interop with Compose | ViewBinding, `viewLifecycleOwner`, ListAdapter + DiffUtil | `views.md` |
| State lost on rotation, or after the app sat in the background | Three tiers: recomposition, config change, process death (Rule 4) | `lifecycle.md` |
| "Where does this code go", DI wiring, or a Flow that emits wrong | Layering, Hilt/Koin, `stateIn` + `repeatOnLifecycle` | `architecture.md` |
| Room migration, DataStore, files, or a query on the main thread | Migration test first, then `fallbackToDestructiveMigration` never | `data.md` |
| Background work never runs, is late, or the service crashes at start | Guaranteed-eventually vs user-visible-now vs exact (Rule 6) | `background.md` |
| Permission denied with no dialog, or scoped storage blocks a file | Rationale state machine, then the policy-restricted list | `permissions.md` |
| Requests fail on device but succeed from the terminal | Cleartext policy, then DNS/proxy, then pinning, then timeouts | `networking.md` |
| Reproducing a bug on a device, or instrumenting one | `logcat --pid`, `dumpsys`, StrictMode, forced Doze, deep-link intents | `debug.md` |
| A crash or ANR reported from the field | Deobfuscate, cluster by signature, then match the ANR timeout | `crashes.md` |
| Slow cold start, jank, oversized bundle, or memory growth | Measure with the macrobenchmark before changing anything | `performance.md` |
| Tests flake, or instrumented tests need a device | Animations off, dispatcher injected, orchestrator with `clearPackageData` | `testing.md` |
| Signing, keystore, R8 keep rules, versionCode, building the AAB | Upload key vs app signing key; mapping file per versionCode | `release.md` |
| Play rejection, staged rollout, tracks, declarations, vitals | The declaration forms are the rejection surface, not the code | `play-console.md` |
| Build, sign, or upload from CI | Keystore as a decoded secret, Gradle cache, emulator with KVM | `ci.md` |
| Foldables, tablets, Wear, TV, Auto, or an OEM-only bug | Window size classes, and the OEM battery ladder for background bugs | `devices.md` |
| Exported components, WebView, backup, deep-link hijack, secrets in the APK | Nothing in the APK is a secret (Rule 7); everything else is attack surface | `security.md` |
| Anything else Android | Name the layer — build, install, runtime, store — then reproduce on one device with one variable changed at a time | — |
Coverage map: `build-failures.md` error string→cause · `gradle.md` build system and dependencies · `sdk-upgrades.md` API-level migration · `compose.md` Compose UI and state · `views.md` XML/Fragments/RecyclerView · `lifecycle.md` state survival and navigation · `architecture.md` layering, DI, coroutines · `data.md` Room, DataStore, storage · `background.md` WorkManager, services, alarms · `permissions.md` runtime and policy permissions · `networking.md` HTTP stack and connectivity · `debug.md` on-device diagnosis toolkit · `crashes.md` field crashes, ANRs, vitals · `performance.md` startup, jank, size, memory · `testing.md` unit, instrumented, screenshot · `release.md` signing, R8, bundles · `play-console.md` tracks, rollout, policy · `ci.md` automated build and upload · `devices.md` devices, emulators, form factors · `security.md` hardening and app surface.
## Core Rules
1. **Name the layer before the fix — build, install, runtime, or store.** "It does not work" means four different investigations, and the wrong one costs the session. The artifact tells you where you are: no APK/AAB produced → build; artifact exists but the device refuses it → install; it runs and misbehaves → runtime; it runs fine and Play says no → store.
2. **The toolchain is one pinned set, upgraded one axis at a time.** AGP, Gradle, the JDK, the Kotlin plugin and the Compose BOM move together; a mismatch surfaces as an error about none of them. Floor rule: `AGP >=8` requires JDK 17, `AGP 7.x` required JDK 11 — `Unsupported class file major version` and `Unable to make field private final java.lang.String java.io.File.path accessible` are both this mismatch, not a code bug. Declare every version in `libs.versions.toml`, never inline, and write the aligned set to `## Toolchain` in `memory.md` the moment it builds green (`gradle.md`).
3. **`targetSdk` is a contract you opt into; `minSdk` is a bill you pay.** Raising targetSdk activates every behavior change up to that level at once (→ Target SDK Breakage) — so raise it deliberately, one level per release, with the compatibility toggles used to test each change before flipping. Raising minSdk deletes compatibility code and deletes users: check the current install-base distribution before moving it, and treat anything below API 24 as a desugaring-and-multidex tax. Play's rule is a moving deadline, not a fixed number — new apps and updates must target within about a year of the latest major release, enforced each 31 August (`play-console.md` carries the current level and its verification date). While `target_sdk` is unset, name the level you are assuming before writing manifest or Gradle code.
4. **Every state holder answers two questions: does it survive rotation, and does it survive process death?** Three tiers, and picking the wrong one is the most common Android bug: `remember`/local field survives recomposition only; `ViewModel` survives configuration change but dies with the process; `SavedStateHandle` and `rememberSaveable` survive both — inside a hard size limit, because saved state crosses a Binder transaction capped at ~1 MB *shared per process*, so keep a screen's saved state in the low tens of KB and store the rest by id. Verify with "Don't keep activities" plus `adb shell am kill <pkg>`, never by rotating alone (`lifecycle.md`).
5. **The main thread has a 16.67 ms frame budget at 60 Hz (8.33 ms at 120 Hz) and a 5 s input-ANR ceiling.** Frames dropped ≈ `work_ms ÷ frame_budget_ms`, so a 100 ms main-thread parse on a 60 Hz device drops 6 frames and reads to the user as a stutter; the same work in a loop reads as a freeze and then an ANR. Disk, network, JSON, image decode, crypto and database access are off-thread by default — StrictMode in debug builds is how you find the ones you forgot (`performance.md`).
6. **Background work is a request the system may refuse.** Choose by the promise you need: guaranteed-eventually → WorkManager (periodic work has a hard 15-minute minimum interval and a flex window; the system decides the moment); user-visible-now → a foreground service, which must call `startForeground()` within ~5 s of being started or the system kills it, and which needs a declared type on API 34+; genuinely time-critical → an exact alarm, which is a permission-gated capability Play restricts to alarm-and-calendar apps. Everything else is best-effort and will not run in Doze or a low App Standby bucket (`background.md`).
7. **Nothing shipped in the APK is a secret.** An APK is a zip: `BuildConfig` constants, `strings.xml`, the manifest, resources and native libraries all come out with public tooling in under a minute, and obfuscation renames symbols without hiding string literals. Any value that authenticates lives behind a backend you control, fetched per session; the client holds at most a short-lived token (`security.md`).
8. **R8 is on in release from the very first release.** Enabling minification later turns every reflection assumption into a launch-day crash in a build nobody can reproduce. `minifyEnabled true` plus resource shrinking from v1, keep rules narrowed to what actually reflects (JSON model classes, anything loaded by name), and the `mapping.txt` for every uploaded versionCode preserved — a stack trace without its mapping file is unreadable forever (`release.md`).
9. **A release writes its row before the rollout starts, not after.** versionCode, versionName, track, rollout percentage, the commit or tag, and where the mapping file went, into `releases/<year>.md` (`memory-template.md`); the crash-free and ANR numbers get appended to the same row after ~48 hours of rollout. Play has no downgrade: rolling back means halting the rollout and shipping a *higher* versionCode built from the previous tag, which is only possible if the tag and its toolchain were written down at the time.
## Target SDK Breakage
What activates the moment `targetSdk` reaches the level — regardless of the device's own version. Compile against the newest SDK freely; this table is about the target. Per-level migration procedure, detection commands and compatibility toggles: `sdk-upgrades.md`.
| targetSdk | What starts applying to your app |
|---|---|
| 23 (6.0) | Runtime permissions: dangerous permissions are requested at use, not granted at install; Doze begins |
| 24 (7.0) | `file://` URIs across app boundaries throw `FileUriExposedException` — a `FileProvider` becomes mandatory for sharing |
| 26 (8.0) | Background execution limits, notification channels required, most implicit broadcasts no longer delivered |
| 28 (9.0) | Cleartext HTTP blocked by default; non-SDK interface restrictions begin; foreground services need their permission |
| 29 (10) | Scoped storage (still opt-out-able); background location becomes a separate prompt; starting activities from the background is restricted |
| 30 (11) | Scoped storage enforced; package visibility requires `<queries>`; unused-permission auto-reset |
| 31 (12) | Every `PendingIntent` must declare mutability; every component with an intent filter must declare `android:exported`; exact alarms become permission-gated; splash screen API applies to all launches |
| 33 (13) | `POST_NOTIFICATIONS` becomes a runtime permission — apps that never asked stop being seen; media permissions split by type |
| 34 (14) | Foreground services require a declared type plus its permission; runtime-registered receivers must declare exported or not; exact-alarm eligibility narrows further |
| 35 (15) | Edge-to-edge display is applied by default (your content draws under the bars); long-running `dataSync` foreground services get a per-day time cap; 16 KB-page native builds become the direction of travel |
| 36+ | Recent levels have continued down the same three roads: display insets, background-work limits, and large-screen behavior. Verify the current level's list against the official behavior-changes page before planning the upgrade — this row exists so nobody assumes the table ends where it was last edited |
## Failure Signatures
Decode rule: the string names the layer. A Gradle stack trace is the build; an `INSTALL_FAILED_*` is the device's package manager; a Java exception class is the runtime; a policy code is Play.
| Signature | Most likely cause | First move |
|---|---|---|
| `Duplicate class X found in modules …` | Two dependency paths pull the same library under different coordinates | `dependencyInsight` on the class's group, then exclude or align (`build-failures.md`) |
| `Unsupported class file major version 6x` | Gradle running on a JDK newer than it supports | Match the JDK to AGP (Rule 2), set it in Gradle's JVM, not just in the IDE |
| `Manifest merger failed : Attribute … is also present at …` | Two manifests declare the same attribute differently | Read the merged manifest report, then `tools:replace` only the attribute you own |
| `Android resource linking failed` | A resource, style parent, or attribute that does not exist at this `compileSdk` | The line is in the message — usually a new attribute against an old compileSdk |
| `Cannot fit requested classes in a single dex file` | Over 65,536 methods without multidex | Enable multidex (automatic on `minSdk >= 21`), then shrink with R8 rather than living with it |
| `INSTALL_FAILED_UPDATE_INCOMPATIBLE` | The installed app is signed with a different key | Uninstall first; on a user's device that means the signing identity changed — never ship that |
| `INSTALL_FAILED_TEST_ONLY` | The APK is flagged test-only, as IDE-run debug builds are | Install with `-t`, or build a real debug APK from the assemble task |
| `INSTALL_FAILED_VERSION_DOWNGRADE` | Lower versionCode than what is installed | Uninstall for local work; on Play there is no downgrade at all (Rule 9) |
| `ActivityNotFoundException` on a deep link | No component matches the intent, or the link is not verified | Resolve the intent from the shell, then check App Links verification (`security.md`) |
| `SecurityException: … PendingIntent … must be mutable/immutable` | Missing the mutability flag required from targetSdk 31 | `FLAG_IMMUTABLE` unless the receiver must fill in the intent (`sdk-upgrades.md`) |
| `ForegroundServiceDidNotStartInTimeException` | `startForegroundService()` without `startForeground()` inside the window | Post the notification first thing in `onCreate`, before any work (`background.md`) |
| `NetworkOnMainThreadException` / `IllegalStateException: Cannot access database on the main thread` | I/O on the UI thread (Rule 5) | Move to a background dispatcher; never use the "allow main thread" escape hatches |
| `IllegalStateException: Can not perform this action after onSaveInstanceState` | A fragment transaction after the state was saved | Do the transaction in a lifecycle-aware place; `commitAllowingStateLoss` hides the bug (`views.md`) |
| `ClassNotFoundException` / `NoSuchFieldException` only in release | R8 removed or renamed something reached by reflection | Add the narrowest keep rule and re-test the release build, not the debug one (`release.md`) |
| `TransactionTooLargeException` | More than the ~1 MB Binder budget crossed at once, usually saved state or an intent extra | Pass an id, load the payload on the other side (Rule 4) |
| ANR with `Input dispatching timed out` | The main thread was blocked past 5 s | Read the ANR trace's main-thread stack — the top frame is the block (`crashes.md`) |
| Anything else | Reproduce on one device, change one variable, and match the exact string | `build-failures.md` for build, `debug.md` for runtime |
## Limits That Force Designs
Constraints that have each killed a half-built Android design.
| Limit | Value | What it decides |
|---|---|---|
| Dex methods per file | 65,536 | Multidex is automatic on `minSdk >= 21`; past that the real cost is startup and build time, so shrink dependencies instead of celebrating multidex |
| Binder transaction | ~1 MB, shared across the whole process | Saved state, intent extras and bound-service payloads all draw on it — pass ids, not blobs (Rule 4) |
| Input ANR | 5 s | Every main-thread operation budget derives from this; broadcasts, services and providers have their own shorter or longer ceilings (`crashes.md`) |
| Foreground service start | ~5 s from start to `startForeground()` | Notification construction happens before any initialization work |
| WorkManager periodic work | 15-minute minimum interval | Anything more frequent is either a foreground service or nothing |
| versionCode | Max 2,100,000,000, must strictly increase | Rules out timestamp schemes in milliseconds; a date-plus-counter scheme fits comfortably (`release.md`) |
| Bundle download size | Play caps the compressed download of the delivered app; larger payloads move to asset packs or on-demand delivery | Ship media as an asset pack rather than fighting the cap (`play-console.md`) |
| Notification channel | Immutable after creation, except by the user | Get importance right the first time; changing it means a new channel id and a migration |
| Activity result / permission request | Must be registered before the lifecycle reaches STARTED | Registration happens at field initialization, not inside a click handler (`lifecycle.md`) |
## Defaults That Decide Behavior
Android's defaults are chosen for compatibility, not for your app. Each of these has produced a bug that reads as an application defect.
| Default | Value | Why it bites |
|---|---|---|
| `android:allowBackup` | true | App data leaves the device through backup unless `dataExtractionRules` restrict it (`security.md`) |
| Configuration change | The Activity is destroyed and recreated | Anything held in the Activity is gone; `android:configChanges` suppresses recreation and creates a second, subtler class of bug (`lifecycle.md`) |
| `collectAsState` on a Flow | Keeps collecting while the app is in the background | `collectAsStateWithLifecycle` is the default choice on Android; the plain version burns battery and hits dead views (`architecture.md`) |
| Room queries | Throw on the main thread — unless the escape hatch is enabled | Enabling `allowMainThreadQueries` converts a crash into an ANR (`data.md`) |
| HTTP client timeouts | OkHttp connect/read/write default to 10 s, with **no** overall call timeout | A stalled response can hang a request indefinitely; set `callTimeout` explicitly (`networking.md`) |
| Bitmap config | `ARGB_8888` → 4 bytes per pixel | A 4000×3000 photo is ~48 MB decoded, whatever the JPEG weighed — always decode with a target size (`performance.md`) |
| `WebView` | JavaScript disabled, but file access and mixed content settings are permissive on older targets | Enable exactly what the page needs; a JavaScript interface is a remote-code surface (`security.md`) |
| App Standby bucket | Assigned by usage, invisible to the app | The same code runs on schedule on your desk and hours late on a device the user rarely opens (`background.md`) |
| OEM battery management | Additional, undocumented, and off-spec | Background work verified on a Pixel can be killed outright on several popular skins (`devices.md`) |
## Output Gates
Before emitting a Gradle file, a manifest entry, a screen, or a release command:
- Is the layer named — build, install, runtime, or store — and does the fix act on that layer (Rule 1)?
- Do the versions in this snippet form one valid set (AGP, Gradle, JDK, Kotlin, Compose BOM), and is the assumed `target_sdk` stated when it is unset (Rules 2, 3)?
- Does every new state holder answer "survives rotation?" and "survives process death?" (Rule 4)?
- Is anything on the main thread that touches disk, network, database, image decode, or crypto (Rule 5)?
- Does any new permission, foreground service type, or exported component carry the manifest declaration *and* the Play declaration it will need (`permissions.md`, `play-console.md`)?
- Would this survive R8 — is anything reached by reflection covered by a keep rule (Rule 8)?
- Is any command destructive (`adb uninstall`, `pm clear`, wiping an emulator, `--rm` on a build directory, halting a rollout)? Then it names exactly what dies and ships with a confirmation step, never inside a copy-paste block of read-only commands.
- Did anything durable come out of this — a release, a toolchain set, a device, a measured number, a non-obvious cause, a declaration text, a decision? Then it is written to its box in `memory-template.md`, with its `## Boxes` line, in this same turn.
## Configuration
Auf GitHub ansehen