An AI-Assisted targetSdk Upgrade: A Week, Not a Quarter
Most of a targetSdk upgrade is proving that documented behaviour changes don't affect you. Turning the official list into executable searches compresses that from weeks to days — plus the compatibility framework for isolating each change, and the three things the model reliably got wrong.
Hessam Rastegari
Senior Android Developer · 12 years shipping Android
TL;DR — A targetSdk bump is mostly proving negatives: forty documented behaviour changes, of which
maybe eight touch you. Compress it by pasting the official behaviour-changes page (never let the model
recall API-level specifics — that's where it's least reliable and most confident) and asking it to convert
each item into a ripgrep pattern. Run them; the empty results are the deliverable. Then use the
compatibility framework (adb shell am compat enable) to test each change in isolation before flipping
targetSdk. Search and triage went from ~3 weeks to ~2 days. Testing didn't compress, and the blocked
dependency took exactly as long as it always does.
Where the quarter actually goes
The instinct is that a targetSdk upgrade is slow because fixing the breakages is slow. It isn't. Most
changes need a handful of lines.
It's slow because of the audit. Google publishes a list of behaviour changes that apply to apps targeting the new level. Forty-odd items covering foreground services, permissions, storage, background launches, non-SDK interfaces, media, notifications. For each one, somebody has to answer: does this affect us?
That's forty investigations across a codebase with a few thousand files, most concluding "no". The conclusions are worthless individually and essential collectively — you can't ship without them, and nobody can review them.
That's the part a model compresses, because it's high-volume mechanical search work with a cheap check.
Step 1: paste the authoritative list — don't ask for it
This is the step that keeps everything downstream honest.
Open the official Behavior changes: apps targeting API level N page and paste the content in. Do not ask the model what changed in the new API level.
Its knowledge of the most recent platform release is precisely where it's weakest and most confident. In my run it described one change with the wrong trigger condition and one that had been announced and then deferred. Both were stated with the same fluency as the correct items, and both would have cost a day.
The rule generalises: for anything version-specific and recent, supply the source. The model's job is to transform it, not to remember it.
Step 2: turn prose into executable searches
The prompt that does the work:
Below is the official list of behaviour changes for
apps targeting API 36.
For each item, give me a ripgrep pattern that would
find potentially affected code in an Android codebase
(Kotlin, Java, AndroidManifest.xml, Gradle).
Format: one line per item — item name, then the pattern.
No explanations. If an item can't be found by search,
say "manual review" and why.
The output is a checklist you can execute:
rg -n "startForeground\(" --type kotlin --type java
rg -n "android:foregroundServiceType" app/src/main/AndroidManifest.xml
rg -n "MediaStore\.|getExternalStorageDirectory" -t kotlin
rg -n "PendingIntent\.(getActivity|getBroadcast)" -t kotlin
rg -n "Settings\.canDrawOverlays|SYSTEM_ALERT_WINDOW"
rg -n "registerReceiver\(" -t kotlin -t java
rg -n "\.exec\(|Runtime\.getRuntime"
The "manual review" escape hatch matters. Some changes — background activity launch restrictions, exact
alarm policy — genuinely can't be grepped. Having them explicitly listed as unsearchable means they get
scheduled rather than silently skipped, which is the usual failure of an ad-hoc audit.
Step 3: run them, and treat the empty results as output
$ rg -n "getExternalStorageDirectory" -t kotlin
$ echo $?
1 # no matches
Thirty of forty items resolve to nothing in about four minutes. Those empty results are the deliverable — they're the evidence that turns "we think we're fine" into an audit somebody can review.
Write them down:
## targetSdk 36 audit — 2026-10-29
| Change | Pattern | Hits | Status |
|---|---|---|---|
| FGS type required | `startForeground\(` | 3 | fix needed |
| Non-SDK interface list | `getDeclaredMethod` | 0 | clear |
| Exact alarms | manual | — | reviewed, no usage |
That table is the artefact the whole exercise exists to produce, and previously it took three weeks to assemble.
Step 4: test each change in isolation, before flipping targetSdk
The most underused tool in this entire process, and it's built into the platform.
The compatibility framework lets you enable a single behaviour change on your current targetSdk:
# List every gated change and its state for your app
adb shell am compat list com.your.app
# Turn on exactly one
adb shell am compat enable FOREGROUND_SERVICE_TYPE_REQUIRED com.your.app
# ...test... then
adb shell am compat reset-all com.your.app
Instead of bumping targetSdk and meeting forty changes at once — where every failure has forty candidate
causes — you enable one, test, fix, reset, move on. Each failure has exactly one possible explanation.
This is also what makes the work parallelisable across a team, and what lets you start before the upgrade branch exists.
Step 5: the model helps with the fixes, one category at a time
Same discipline as any bulk change: one behaviour change per commit, three worked examples, an explicit instruction to flag anything that doesn't fit rather than improvising.
Category: foreground service types.
Here are two services already converted: [paste].
Apply the same pattern to these 3.
If a service's type isn't obvious from what it does,
list it and skip — don't guess.
The "don't guess" clause earns its place here specifically: picking a foreground service type wrong doesn't fail to compile, it fails at runtime on a user's device, months later, as a crash on service start.
What it got wrong
1. API-level specifics. Covered above, and it's the big one. Paste the source.
2. @SuppressLint offered as a remedy. Repeatedly. Lint is warning you about the behaviour change;
suppressing it removes the warning and keeps the breakage. Any suggestion of the form "add this annotation
and it'll build" needs reading twice.
3. It has no idea which dependency is your actual blocker. In practice the critical path of a
targetSdk upgrade is usually one SDK — an analytics library, a payment provider, a DRM component — with no
release compatible with the new level. No amount of prompting finds that; you find it by building and
reading the error, and you resolve it with an email, a GitHub issue, or a vendor call.
Start there. Check your dependencies for compatible releases on day one, in parallel with the audit, because it's the longest pole and it's the one you don't control.
The honest accounting
| Phase | Before | After |
|---|---|---|
| Audit: 40 changes × search + triage | ~3 weeks | ~2 days |
| Writing the fixes | ~1 week | ~3 days |
| Testing (instrumentation, devices, QA) | ~2 weeks | ~2 weeks |
| Blocked dependency (vendor) | 3 weeks | 3 weeks |
The audit collapsed. Writing fixes improved somewhat. Testing didn't compress at all, and neither did waiting for a third party — which together are now the majority of the timeline.
That's the real shape of these wins: the mechanical, high-volume, cheap-to-verify half compresses hard, and
everything else stays exactly where it was. Anyone reporting a targetSdk upgrade done in a day either has
a very small app or didn't test it.
The rule
The model turns documentation into a checklist; you still have to walk it. Supply the official source
rather than trusting recall, convert each item into a search you can run, treat the empty results as the
audit artefact, and isolate each behaviour change with am compat before you flip anything. Then start the
dependency conversation on day one, because that's the part nothing here can speed up.