On a Tuesday morning you clone the repo, open the mobile AI module, and the very first file you see carries a comment that says do not touch. Your instinct is to read the inference code until it makes sense, but the model file is not what is broken on your machine. The phone in your hand is a different environment from the one the original author used, and nobody wrote that difference down anywhere. Spend your first hour collecting a receipt for that environment, then spend your second hour breaking the network path on purpose.
The first hour is for facts, not for the model file
A receipt is a small YAML file that you generate on a real device and attach to your first pull request. It answers the questions a reviewer would otherwise guess at when they read your bug report. If you cannot fill a field honestly, write null instead of a plausible number, because a guessed value is worse than a gap.
The receipt should cover four things: the device and OS build that produced your observation, the permission state at the moment of the call, whether the first inference ran on device or was served remotely, and the network and power conditions during that call. None of those fields require you to understand quantization, tokenizers, or prompt templates. They only require you to stop treating your laptop and your phone as the same machine.
Start with the facts the shell already knows
You do not need a new dependency for the first half of the receipt. The platform tooling will tell you almost everything:
adb shell getprop ro.product.model
adb shell getprop ro.build.version.release
adb shell getprop ro.product.cpu.abi
adb shell dumpsys package com.example.app | grep -E "versionName|versionCode"
adb shell cmd package check-permission android.permission.RECORD_AUDIO com.example.app
Paste those values into a file named receipt-<your-handle>.yaml and commit it next to your fix. The shape matters more than the tooling, so keep the schema boring and comparable across machines:
captured_at: 2026-11-08T09:42:00Z
device:
model: <from getprop>
sdk: <from getprop>
abi: <from getprop>
app:
version_name: <from dumpsys>
build_type: debug
permissions:
RECORD_AUDIO: granted
POST_NOTIFICATIONS: denied
network:
transport: wifi
metered: false
power:
battery_pct: <from dumpsys battery>
saver: false
charging: false
inference:
first_call_ms: null
warm_call_ms: null
source: on_device
Two juniors on the same commit can now compare their environments without a thirty-minute call. That is the entire point of the artifact, and it is why a receipt belongs in the pull request rather than in a chat thread.
Time the first call once, and label it as cold
"It feels slow on my phone" is not a report anyone can act on. Wrap the very first call after process start and record the elapsed time in milliseconds, then do the same for a second call on a warm runtime. Keep the two fields separate, because the difference between them is usually the part that your users actually feel.
The snippet below is a sketch, not a library: it shows where the clock belongs and which label you owe the reader.
val t0 = SystemClock.elapsedRealtime()
val first = engine.infer(prompt) // first call after process start
val coldMs = SystemClock.elapsedRealtime() - t0
val t1 = SystemClock.elapsedRealtime()
engine.infer(prompt)
val warmMs = SystemClock.elapsedRealtime() - t1
// write coldMs and warmMs into the receipt, or null if you skipped the step
Record the power state at the same time, because a device in battery saver is a different test subject. If you only have one phone, say so in the receipt instead of implying a matrix you never ran. One honest device beats four invented ones.
Now break the fallback on purpose
Most onboarding bugs in mobile AI features live in the path that runs when the remote call fails, not in the path that succeeds. To rehearse that path you need a real upstream you can kill at will, and you should never rehearse it against production credentials. MonkeyCode is an open-source project whose operator describes free model access and a free server option; the disclosure below applies to that mention, and you should confirm the current terms on the project page before you rely on them.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
That combination is genuinely useful for a first-week rehearsal, because a hard-coded stub will not reproduce streaming behavior, timeout shapes, or the latency variance your UI has to survive. A real endpoint that you can stop, restart, and misconfigure gets you much closer to the failure your users will eventually hit. The steps below are the sequence I would hand to a new teammate; they describe a procedure, not measured results, so your receipt is the only evidence that counts.
- Deploy a minimal service with
/healthand a single completion route on the free server option, and keep it separate from any environment that holds real user data. - Point only the debug build at that host through a build config field, and assert in code that the release build cannot read it.
- Send a synthetic fixture rather than a real recording, then confirm the response renders with a visible source label such as remote or on-device.
- Stop the service and repeat the same user flow, noting whether the app shows an error, retries silently, or falls back to the on-device path.
- Revoke the microphone or notification permission between two sessions, then relaunch and repeat step four with the same fixture.
Keep the test data synthetic
Never push real audio, transcripts, or photos through a shared free endpoint, even when the endpoint belongs to a project you trust. Generate a fixture with an obvious placeholder string and commit it to the repo so every teammate rehearses with identical input. If your product handles regulated data, treat the free tier as a scratchpad for schema and status codes only.
A failure matrix you can finish before lunch
Write the expected behavior down before you run the transition, otherwise you will rationalize whatever you observe. The table below is the one I use; each row takes a few minutes on a single device.
| Transition | What you expect | Where the receipt records it |
|---|---|---|
| Cold start with airplane mode on | Fallback to on-device or a clear offline state |
network.transport, inference.source
|
| Upstream returns 500 | Visible error, no silent retry storm | request log plus source
|
| Upstream hangs past your timeout | Timeout fires and the UI leaves the spinner | first_call_ms vs timeout config |
| App backgrounded mid-request | Request survives, is cancelled, or is re-owned |
source after resume |
| Permission revoked before resume | Prompt appears, no crash, no stale cache read | permissions |
| Device reboot with a queued item | Queue drains once, not twice | queue depth after launch |
The last column matters most, because it tells the reviewer which field would have caught the bug earlier. If a row has no matching field, you just found a gap in the receipt schema, and closing that gap is a legitimate first contribution. That is a much better first PR than a speculative refactor of code you have not run yet.
What this receipt does not prove
A receipt is evidence about one build on one device at one moment. It does not prove that your feature is fast, that it is battery-friendly, or that it behaves the same on a mid-range phone you have never touched. It also says nothing about R8 shrinking, release signing, or Play-delivered split APKs, all of which can change startup behavior in ways a debug build hides.
Privacy claims need a separate review, because a granted permission at capture time tells you nothing about retention, backup, or what leaves the device. Keep the receipt factual and let the reviewer draw the conclusions from the fields you recorded.
Who should skip this workflow
Skip it if your team has no physical test device, since an emulator receipt will mislead you about cold-start cost and sensor behavior. Skip it if your compliance rules forbid sending any fixture to a third-party endpoint, because the fallback rehearsal depends on a remote host you control. Skip it too if you already run a device farm with per-build captures, since your existing artifacts are stronger than a hand-written YAML file.
For everyone else, the payoff arrives on day two, when someone asks why the transcription feature lost its first request after a phone call. You will have a file that says which device, which permission state, and which transition, and you can answer in one message instead of a week of guessing. Then you can genuinely fix the thing instead of re-reading the file that says do not touch.
If you run this, reply with your device model, OS version, and the transition you tested, plus whether the request recovered, restarted from scratch, or silently disappeared. Comparable evidence from a second environment is worth more than another opinion about the model code.
Top comments (0)