<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roronoa</title>
    <description>The latest articles on DEV Community by Roronoa (@roronoa_).</description>
    <link>https://dev.to/roronoa_</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4022649%2Fac571ee6-4982-49a9-8a57-3eb9fb326cc2.jpg</url>
      <title>DEV Community: Roronoa</title>
      <link>https://dev.to/roronoa_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/roronoa_"/>
    <language>en</language>
    <item>
      <title>Keep Partial Transcripts Out of Crash Upload Payloads</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Thu, 24 Sep 2026 18:22:57 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-partial-transcripts-out-of-crash-upload-payloads-3ao8</link>
      <guid>https://dev.to/roronoa_/keep-partial-transcripts-out-of-crash-upload-payloads-3ao8</guid>
      <description>&lt;p&gt;You join the voice-notes repo on a Monday morning, and the open bug is already older than your laptop setup. The on-device recognizer drops the last partial utterance when an incoming phone call kills the app process. In the first hour you add a crash breadcrumb that stores that partial text so the next fatal error is easier to explain. By the afternoon that same string sits inside a vendor upload collected from the shared QA phone on the desk.&lt;/p&gt;

&lt;p&gt;This opening is a proposed failure path, not a measurement taken from a named handset anywhere in this article. You should treat every version, permission bit, and power state below as a field you fill on your own device. The goal is to stop your first pull request from teaching the crash pipeline how to remember spoken content. A redacted breadcrumb can still explain the process death without copying the utterance into the outbound upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you inherit in the first hour
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Separate this leak from older ones
&lt;/h3&gt;

&lt;p&gt;You are not retesting keyboard learning, phone backup, recents capture, or the remote debug host during this pass. Those leaks need their own experiments, and mixing them in one matrix hides which control actually failed. This pass covers crash breadcrumbs, native tombstones, and the log buffer that a reporter attaches after the process dies. If a unique sentence can be rebuilt from any of those three places, the first-hour patch is not finished.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write the environment header first
&lt;/h3&gt;

&lt;p&gt;Write the device model, OS build, framework version, and crash SDK version at the top of your notes. Also record microphone permission, network type, and whether the phone is on battery saver or wall power. A junior rollback is safer when that header remains in the pull request after the diff has already moved. Without the header, the next person cannot separate a scrub failure from an OS change they never reproduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what a breadcrumb may keep
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use a contract, not a vibe
&lt;/h3&gt;

&lt;p&gt;Use the table as the contract for the first pull request, and review every row before you add an SDK call. It is a review aid, not a claim that every crash vendor behaves the same way on every OS build. If your pinned SDK disagrees with a row, follow the current vendor guide and record that exception in the pull request. Do not copy an older sample that attaches the raw message string just because a tutorial did that last year.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Allowed in crash payload&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Partial transcript text&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Speech content is user data, even when recognition ran on device.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transcript length bucket&lt;/td&gt;
&lt;td&gt;Yes, coarse only&lt;/td&gt;
&lt;td&gt;Empty, short, or long helps triage without storing words.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locale tag&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Language id explains model choice and does not reveal the utterance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model package id&lt;/td&gt;
&lt;td&gt;Yes, if non-secret&lt;/td&gt;
&lt;td&gt;Identifies the on-device bundle you shipped, not the audio.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio bytes or file path&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Paths often include user ids, and bytes are the recording itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission state at crash&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Revoked versus granted microphone changes the expected failure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle transition&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Background, call interruption, or low-power entry is the variable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exception class and stack&lt;/td&gt;
&lt;td&gt;Yes, after scrub&lt;/td&gt;
&lt;td&gt;Keep the type, and drop messages that echo the utterance.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When support asks for the words
&lt;/h3&gt;

&lt;p&gt;If a teammate says support cannot debug the death without the words, move that debate out of the first hour. Ship the redacted payload first, then design a consented debug build that never ships on the public store track. A store build that sometimes includes speech is harder to roll back than a flag that never attached the text. Keep the debate in the ticket, and keep the words out of the payload while the team waits for a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the breadcrumb in one place
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Proposed Kotlin scrub
&lt;/h3&gt;

&lt;p&gt;Put the scrub in one function so a later feature cannot invent a second logger that still holds the raw text. The Kotlin snippet below is proposed example code, and you should rename the types to match the crash SDK you link. It has not been timed on a device for this article, so do not read it as a latency or battery result. Run it only after you confirm the method names against the SDK version pinned in your dependency lockfile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;VoiceCrashCrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lengthBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;modelPackageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;micGranted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lifecycle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;errorClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;lengthBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"empty"&lt;/span&gt;
    &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"short"&lt;/span&gt;
    &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"medium"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"long"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;voiceCrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modelPackageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;micGranted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lifecycle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Throwable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;VoiceCrashCrumb&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Never pass partial or error.message into the SDK map.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;VoiceCrashCrumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;lengthBucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lengthBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;modelPackageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modelPackageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;micGranted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;micGranted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lifecycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;errorClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;simpleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Register the same rule on iOS and cross-platform shells
&lt;/h3&gt;

&lt;p&gt;Never pass the partial string or the exception message into the map that the crash SDK will serialize and send. On iOS, apply the same rule to every custom key you set before the reporter builds its outbound payload. Metric and diagnostic APIs differ by OS version, so read the guide that matches the SDK version you actually link. If that SDK offers a log scrubber callback, register it beside the crumb builder rather than in a random extension.&lt;/p&gt;

&lt;p&gt;A second registration point is how the first rollback leaves one path still attaching speech after the flag looks off. Search the repo for the partial variable name before review, including instrumentation tests and leftover sample activities. A unit test that asserts the serialized map lacks that variable is worth more than a comment about being careful. Add the test in the same pull request so a rollback cannot delete the only guard you had.&lt;/p&gt;

&lt;p&gt;If you are in React Native or Flutter, keep the scrub on the native crash bridge, not only in the Dart or JavaScript logger. A redacted JS breadcrumb does nothing once the native module still forwards the utterance in its own map. Name that bridge method in the pull request so the reviewer knows which side you actually changed. Cross-platform shells fail this test when only one language was searched for the partial string before review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the first-hour experiment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Follow one order
&lt;/h3&gt;

&lt;p&gt;The steps below are a procedure for you to run, not a result reported from a lab phone in this article. Fill the environment header, follow the order, and stop when the payload still contains the unique synthetic sentence. A pass on a plugged-in phone does not prove the same scrub under battery saver or after a microphone revoke. Those transitions belong in this pull request because they are how on-device voice sessions actually die in the field.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Record device model, OS version, app version, crash SDK version, network type, battery saver state, and microphone permission before you start.&lt;/li&gt;
&lt;li&gt;Install a debug build that calls the crumb builder, and keep any raw logger behind a flag that defaults to off.&lt;/li&gt;
&lt;li&gt;Speak a unique synthetic sentence that appears nowhere else in the repo, using an animal name plus a four-digit code.&lt;/li&gt;
&lt;li&gt;Force a crash from the voice screen after the partial text is visible, using the test hook you already ship for QA.&lt;/li&gt;
&lt;li&gt;Collect the local crash file or the debug upload, then search for that sentence, the raw partial, and the exception message.&lt;/li&gt;
&lt;li&gt;Repeat the crash once after you background the app, and once after you revoke the microphone and return to the same screen.&lt;/li&gt;
&lt;li&gt;Toggle airplane mode before the reporter flushes, restore the network, and search the retried payload for the same sentence.&lt;/li&gt;
&lt;li&gt;Confirm the payload still has the length bucket, locale, model package id, permission bit, lifecycle label, and exception class.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Capture commands you can adapt
&lt;/h3&gt;

&lt;p&gt;Replace the sample package name, and do not collect logcat from a phone that already holds real user audio from customers. If the search tool in the sample is missing on your host, use grep with the same pattern and paste the command into the pull request. On a shared QA phone, clear the log buffer before the run so an older session cannot masquerade as a fresh pass. A dirty buffer is the usual reason a junior marks the scrub finished while yesterday's sentence is still sitting there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell getprop ro.build.version.release
adb shell dumpsys package com.example.voicenotes | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; permission
adb logcat &lt;span class="nt"&gt;-c&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; 200 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"transcript|voiceCrumb|PARTIAL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Record the iOS outcome separately
&lt;/h3&gt;

&lt;p&gt;iOS needs a different capture path, because Console and the crash reporter do not share a single ring buffer. Export the pending report from the device, or open the SDK debug viewer, then search for the same unique sentence. Write whether that report recovered after relaunch, restarted as a new session, or silently disappeared from the pending queue. Keep that observation next to the Android notes, and do not invent a battery figure to make the two platforms look comparable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll back without restoring the leak
&lt;/h2&gt;

&lt;p&gt;Your first rollback should disable the flag that attaches breadcrumbs, not delete the scrub function from the module. If the redacted crumb crashes inside the crash SDK, turn attachment off and keep forbidden fields out of every other logger. Do not fix a bad upload by putting the raw partial back while you wait for a vendor support reply. That shortcut becomes the next release incident, and it is harder to see because the flag was described as temporary.&lt;/p&gt;

&lt;p&gt;A rollback note in the pull request can stay this concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The attachment flag defaults to false on the store track, and a debug build may enable it only after the unique-sentence search passes.&lt;/li&gt;
&lt;li&gt;Reverting that flag does not revert the rule that partial text never enters the upload map, the log line, or the native message.&lt;/li&gt;
&lt;li&gt;If QA needs the words, they use a local-only overlay that the release build compiles out, rather than a hidden gesture in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the flag-off build still uploads the sentence, the leak sits outside this patch and needs a wider search before you blame the crumb. Check analytics events, native standard error, and any attach-log checkbox that the reporter settings screen still leaves enabled. Also search for string interpolation that builds a log line before your scrub function has a chance to run. The first hour is long enough to miss a debug print that a sample activity left behind during the previous sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Draft fixtures away from production speech
&lt;/h2&gt;

&lt;p&gt;You will want synthetic utterances and a second reading of the scrub rules before you open the first pull request. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that drafting when you paste only synthetic sentences and the redaction function. Do not paste customer audio, device logs, or crash archives just because that server is free and the first hour feels short.&lt;/p&gt;

&lt;p&gt;This article does not name a model, a token quota, a machine shape, or a duration you should plan a release around. An allowance you remember from an older post is not evidence, so treat the live limit as unknown until the project page states it. If the project publishes a repository or a pricing note, read that page yourself rather than trusting a recap from chat. Use the free model access on the free server to generate length-bucket cases from invented sentences only.&lt;/p&gt;

&lt;p&gt;Then ask whether a proposed log line still holds recoverable speech, after you confirm the current terms on the project page. If your workplace forbids external code tools, skip the drafting server and review the contract table with a teammate on your own network. The handset experiment still stands on its own, and a local review is enough to open a careful first pull request. A free server is only a convenience for synthetic fixtures, not a store for the speech this patch is trying to keep on device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should skip this pattern
&lt;/h2&gt;

&lt;p&gt;Skip this pattern when you must retain verbatim speech for a medical, legal, or accessibility record inside an already approved system. Also skip it when the crash SDK cannot attach custom keys without also capturing the full log buffer on that OS version. In that case the first pull request should disable log attachment, rather than encode the sentence into a clever numeric bucket. A one-character length bucket is the transcript with extra steps, and review should reject that encoding as a leak.&lt;/p&gt;

&lt;p&gt;Employee debug screens can remain when they are local-only and compiled out of the release variant you actually ship. They must not call the upload API, even behind a hidden gesture that the QA group believes nobody outside the team will find. Hidden gestures leak because crash reporters and screen recordings both outlive the person who added them during onboarding. If you need consented capture, put it in a separate build flavor whose name a reviewer cannot miss in the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should send back
&lt;/h2&gt;

&lt;p&gt;Reply with the device, the OS build, the crash SDK version, and the lifecycle step you actually executed on that hardware. Say whether the unique sentence recovered in a later upload, stayed only in the local log, or disappeared after the scrub. If you rolled the flag back, say whether the flag-off build still contained the sentence inside some other attachment. Those three outcomes are more useful than a vague note that the crash flow felt acceptable on your desk.&lt;/p&gt;

&lt;p&gt;This article measured no handset, and vendor behavior changes with the SDK version you pin during the following week. A scrub that passes a logcat search can still fail inside a native minidump or an attached console export from the same run. Re-read the SDK notes for your exact dependency before you merge, and link that note from the pull request description. If you cannot name the device and the transition, you do not yet have a result, only a plan you still need to run.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>privacy</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Secure Mobile Handoff for First-Hour Streaming Before Recents Capture</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 23 Sep 2026 16:08:39 +0000</pubDate>
      <link>https://dev.to/roronoa_/a-secure-mobile-handoff-for-first-hour-streaming-before-recents-capture-3o8d</link>
      <guid>https://dev.to/roronoa_/a-secure-mobile-handoff-for-first-hour-streaming-before-recents-capture-3o8d</guid>
      <description>&lt;p&gt;You clone the mobile AI repo on day one, and the onboarding doc tells you to send a test prompt before lunch. The assigned device is still in the foreground, microphone permission is granted, and the streaming view is painting tokens into a chat bubble. You switch apps to copy a ticket number, and the recents thumbnail still shows the last partial answer. An hour later your first rollback lands, yet that snapshot can remain on the device until the next cold start.&lt;/p&gt;

&lt;p&gt;This article is a proposed first-PR checklist, not a measured lab report from a specific fleet. You should treat every command below as a starting procedure and record what your own device actually does. The goal is a secure handoff between on-device streaming and an optional sandbox host. You still must keep first-hour text out of Recents, accessibility captions, and leftover files after rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually goes wrong in the first hour
&lt;/h2&gt;

&lt;p&gt;A junior engineer usually debugs the happy path: a prompt goes in, tokens come out, and the ticket is marked done. Mobile operating systems do extra work whenever you leave the activity, and that work is not in the product spec. Recents capture, accessibility trees, log buffers, and backup-eligible cache files all sit outside your Compose or Flutter widget tree.&lt;/p&gt;

&lt;p&gt;You also get pressure to just hit the cloud so the first demo looks smart. Production inference hosts are the wrong place for that first hour, because test prompts and device identifiers do not belong there. A sandbox host is only acceptable when the prompt is synthetic and the app redacts the payload before it leaves the process.&lt;/p&gt;

&lt;p&gt;Typical first-hour leaks look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recents or App Switcher thumbnails still render the last streamed answer.&lt;/li&gt;
&lt;li&gt;Android accessibility nodes expose the full token buffer to any bound service.&lt;/li&gt;
&lt;li&gt;iOS snapshot APIs write a launch image that survives the next process death.&lt;/li&gt;
&lt;li&gt;Debug builds print function-call JSON to logcat or the Xcode console.&lt;/li&gt;
&lt;li&gt;A rollback restores an old binary but leaves cache files and the last thumbnail.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Proposed lab setup, labeled as unexecuted
&lt;/h2&gt;

&lt;p&gt;Do not copy these versions as if they were measured results from this article. Write down the device you actually hold, including OS build and framework versions. Network, permission, and power state belong in the same note, because each one changes what the operating system captures. The lifecycle transition under test should be a single action, such as Home, Recents swipe, permission revoke, or uninstall.&lt;/p&gt;

&lt;h3&gt;
  
  
  Record before you type a prompt
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Device and OS: Pixel 8a on Android 15, or iPhone 14 on iOS 18, as examples only&lt;/li&gt;
&lt;li&gt;Framework: Android View or Compose, UIKit or SwiftUI, React Native, or Flutter, with exact versions&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi only, cellular only, or airplane mode with a local mock&lt;/li&gt;
&lt;li&gt;Permissions: microphone, notifications, and accessibility, each granted or revoked&lt;/li&gt;
&lt;li&gt;Power: plugged in, or battery saver / Low Power Mode enabled&lt;/li&gt;
&lt;li&gt;Application state: cold start, warm start, or already streaming in the foreground&lt;/li&gt;
&lt;li&gt;Lifecycle transition: Home button, Recents swipe, permission revoke, or uninstall rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected observations belong in your notes, not in this article as claimed numbers. You are looking for whether the answer recovered, restarted from a blank surface, or silently remained visible. If two teammates cannot name the same transition, they are not comparing the same bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The handoff you want on a first PR
&lt;/h2&gt;

&lt;p&gt;You should keep first-hour streaming on-device unless a reviewer explicitly asked for a hosted comparison run. If you need a comparison host, point it at a disposable sandbox rather than the production AI endpoint. Redact the prompt, the tool-call JSON, and any device identifiers before the request is built. Blank the UI before the operating system takes a snapshot, then prove a later rollback deletes the cache.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can serve as that disposable comparison host for non-production first-hour checks. They are not a privacy boundary, so you should still send only synthetic prompts from the debug build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision table for the first-hour path
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;On-device path&lt;/th&gt;
&lt;th&gt;Sandbox host path&lt;/th&gt;
&lt;th&gt;Do not do this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synthetic prompt, no account data&lt;/td&gt;
&lt;td&gt;Allowed for local smoke&lt;/td&gt;
&lt;td&gt;Allowed if URL is compile-time sandbox&lt;/td&gt;
&lt;td&gt;Point at production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real user paste or ticket text&lt;/td&gt;
&lt;td&gt;Keep in memory only&lt;/td&gt;
&lt;td&gt;Block the request&lt;/td&gt;
&lt;td&gt;Log the raw string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microphone buffer present&lt;/td&gt;
&lt;td&gt;Stop capture on background&lt;/td&gt;
&lt;td&gt;Do not upload the buffer&lt;/td&gt;
&lt;td&gt;Attach audio to the comparison call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission revoked mid-stream&lt;/td&gt;
&lt;td&gt;Cancel, clear surface&lt;/td&gt;
&lt;td&gt;Cancel in-flight call&lt;/td&gt;
&lt;td&gt;Retry against production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recents / App Switcher&lt;/td&gt;
&lt;td&gt;Privacy overlay or &lt;code&gt;FLAG_SECURE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same overlay still required&lt;/td&gt;
&lt;td&gt;Trust the sandbox to hide UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First rollback / uninstall&lt;/td&gt;
&lt;td&gt;Delete cache and thumbnails&lt;/td&gt;
&lt;td&gt;Drop sandbox tokens&lt;/td&gt;
&lt;td&gt;Leave comparison URL in &lt;code&gt;SharedPreferences&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Android: stop Recents from painting tokens
&lt;/h2&gt;

&lt;p&gt;The OS will screenshot your activity when the user leaves it, unless you opt out. &lt;code&gt;FLAG_SECURE&lt;/code&gt; is the blunt instrument, and it also blocks intentional screenshots, so QA needs a debug toggle. A first PR should default the streaming activity to secure, then prove Recents no longer shows tokens. You should treat that proof as a device photograph, not as a glance at the emulator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed snippet for the streaming Activity.&lt;/span&gt;
&lt;span class="c1"&gt;// Label: unexecuted example. Confirm against your minSdk and theme.&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StreamingActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_SECURE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_SECURE&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setContentView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activity_streaming&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;streamingController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelAndClearSurface&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;logRedactor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropTokenBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onStop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should also keep the task out of Recents when the first-hour debug build is only a scratch session. Leave the production manifest untouched until a reviewer asks for a persistent task. Debug overlays are the right place for &lt;code&gt;excludeFromRecents&lt;/code&gt;, because a shipping chat screen still needs a normal recents entry after launch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- AndroidManifest.xml, debug overlay only --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;activity&lt;/span&gt;
    &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;".StreamingActivity"&lt;/span&gt;
    &lt;span class="na"&gt;android:excludeFromRecents=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
    &lt;span class="na"&gt;android:autoRemoveFromRecents=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For logcat, print a hash of the prompt rather than the prompt. Juniors often enable verbose OkHttp logging, and that is how function-call JSON leaves the phone. Strip query strings before any debug logger sees the request, even when the host is only a sandbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed interceptor. Unexecuted example.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RedactingInterceptor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Interceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Interceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;safeUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encodedQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ai-handoff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sandbox call ${request.method} ${safeUrl.encodedPath}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  iOS: cover the window before the snapshot lands
&lt;/h2&gt;

&lt;p&gt;UIKit takes a snapshot as you resign active, and that image can appear in App Switcher. A first-hour PR should install a cover view in &lt;code&gt;sceneWillResignActive&lt;/code&gt;, then remove it on &lt;code&gt;sceneDidBecomeActive&lt;/code&gt;. &lt;code&gt;isSecureTextEntry&lt;/code&gt; helps for the prompt field, but it does not cover the answer transcript. Simulator App Switcher is not enough, because physical devices write snapshots on a different timing path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed SceneDelegate hooks. Unexecuted example for iOS 17+.&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;sceneWillResignActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;streamingSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelAndClear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kt"&gt;PrivacyCover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;sceneDidBecomeActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIScene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;PrivacyCover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;PrivacyCover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9001&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewWithTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bounds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;
        &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backgroundColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;systemBackground&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewWithTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeFromSuperview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use SwiftUI, hide the transcript with &lt;code&gt;privacySensitive()&lt;/code&gt; and verify VoiceOver does not read the last answer after backgrounding. That check is easy to skip on a first PR because the happy path still looks correct in the simulator. Walk the VoiceOver cursor after pressing Home, and confirm the cover view is the only thing spoken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flutter and React Native: do not assume the plugin did it
&lt;/h2&gt;

&lt;p&gt;Cross-platform shells often wrap a WebView or a platform view, and &lt;code&gt;FLAG_SECURE&lt;/code&gt; is not automatic. You need a method channel, and you need to call it before the first token arrives, not after the user hits Home. A JavaScript-only overlay can lose the race against the OS snapshot, which means Recents still shows tokens. Put the secure-window call on the native side, then cancel the stream from the same lifecycle hook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed Flutter channel. Unexecuted example.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_secure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MethodChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'handoff/secure_window'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;protectStreamingWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_secure&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'setSecure'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clearStreamingWindow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_secure&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'setSecure'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Android side of the same channel.&lt;/span&gt;
&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"setSecure"&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;enable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;window&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_SECURE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayoutParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_SECURE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed React Native AppState handler. Unexecuted example.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NativeModules&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SecureWindow&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NativeModules&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bindHandoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamingController&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inactive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;background&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;SecureWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;streamingController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelAndClearSurface&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On React Native, confirm the overlay is not a JavaScript-only View, because the OS snapshot can race the JS thread. Call the native module first, then clear the in-memory token buffer. If the buffer still feeds a virtualized list, Recents can capture one extra frame after &lt;code&gt;AppState&lt;/code&gt; says inactive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional sandbox comparison, without a production host
&lt;/h2&gt;

&lt;p&gt;When a reviewer asks whether on-device latency is even in the same band as a hosted model, you still should not open a production endpoint from a debug build. Put the comparison URL behind a debug flavor, and fail the build if a production host sneaks into that flavor. Cancel in-flight sandbox calls in &lt;code&gt;onStop&lt;/code&gt; and &lt;code&gt;sceneWillResignActive&lt;/code&gt; so a comparison request cannot outlive the UI. A request that outlives the UI is how first-hour text ends up in a server log you do not control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed BuildConfig gate. Unexecuted example.&lt;/span&gt;
&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;InferenceHandoff&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;comparisonUrl&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;HttpUrl&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"comparison host is debug-only"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SANDBOX_INFERENCE_URL&lt;/span&gt;
        &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sandbox"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHttpUrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed local check before you open the first PR.&lt;/span&gt;
&lt;span class="c"&gt;# Replace the package name with yours.&lt;/span&gt;

adb shell dumpsys activity recents | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; streaming
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s2"&gt;"prompt=|content=|tool_call"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: raw payload in logcat"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# iOS counterpart: inspect a physical-device screenshot, not only Simulator files.&lt;/span&gt;
&lt;span class="c"&gt;# If tokens are visible, the cover view was installed too late.&lt;/span&gt;
xcrun simctl io booted screenshot /tmp/recents-check.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not treat those commands as a pass/fail score for a device class. They only tell you whether this package, on this build, leaked a string or a thumbnail after one transition. Write the command output into the PR so a rollback review has evidence besides a demo GIF.&lt;/p&gt;

&lt;h2&gt;
  
  
  First rollback must delete more than the binary
&lt;/h2&gt;

&lt;p&gt;Your first PR will get reverted. Rollback is not complete if Recents, cache, and the sandbox token remain. Add an uninstall-and-reinstall pass to the onboarding doc, and write the leftover paths into the PR template. Photograph Recents with a second device, because the phone you are testing cannot see its own thumbnail clearly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Force-stop the app, then open Recents and photograph the thumbnail with a second device.&lt;/li&gt;
&lt;li&gt;Revoke microphone permission while a stream is active, and confirm the surface clears.&lt;/li&gt;
&lt;li&gt;Background the app, resume, and check whether tokens reappear without a new request.&lt;/li&gt;
&lt;li&gt;Uninstall, reinstall the rolled-back build, and search app-specific storage for prompt files.&lt;/li&gt;
&lt;li&gt;On Android, run &lt;code&gt;adb shell pm clear &amp;lt;package&amp;gt;&lt;/code&gt; only on the engineering device, never on a shared dogfood phone.&lt;/li&gt;
&lt;li&gt;On iOS, delete the app from Home Screen, then confirm Files / iCloud Drive has no leftover export.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android leftover hunt after rollback. Engineering device only.&lt;/span&gt;
adb shell run-as com.example.app &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; cache/
adb shell run-as com.example.app &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; files/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any prompt-like file is still there, the first PR is not done. The rollback recovered the old binary, but the device did not recover a clean state. Say that explicitly in the PR so the next junior does not treat uninstall as optional cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not use this
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FLAG_SECURE&lt;/code&gt; and App Switcher covers hide pixels; they do not encrypt memory. Accessibility services you granted for debugging can still read the tree unless you strip content descriptions on background. A sandbox host is shared infrastructure from your point of view as a junior, so real customer text does not belong there even when free model access is available for experiments.&lt;/p&gt;

&lt;p&gt;You should not use this handoff if any of the following is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The prompt contains production user data, credentials, or health information.&lt;/li&gt;
&lt;li&gt;Your team already forbids third-party inference hosts in debug builds.&lt;/li&gt;
&lt;li&gt;You need screenshot-based QA of the streaming UI, and &lt;code&gt;FLAG_SECURE&lt;/code&gt; would block that workflow without a documented debug toggle.&lt;/li&gt;
&lt;li&gt;You are running on a shared device whose Recents or iCloud account you do not control.&lt;/li&gt;
&lt;li&gt;You were about to treat latency numbers from one phone as a benchmark for a whole device class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This procedure also does not prove battery cost, thermal throttling, or model quality. Those need separate measurements on named devices, and this article does not invent those numbers. A clean Recents thumbnail is a privacy check, not a performance result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to report back
&lt;/h2&gt;

&lt;p&gt;If you run the checklist, write down the device, OS, framework versions, permission state, and the exact transition. Then say whether the streamed answer recovered, restarted from a blank surface, or silently stayed in Recents after rollback. Comparable environment evidence is more useful than a screenshot of a happy demo prompt.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>privacy</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Keep First-Hour Model Weights Out of Shared Storage After a Rollback</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:27:44 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-first-hour-model-weights-out-of-shared-storage-after-a-rollback-3olf</link>
      <guid>https://dev.to/roronoa_/keep-first-hour-model-weights-out-of-shared-storage-after-a-rollback-3olf</guid>
      <description>&lt;p&gt;You clone the mobile AI sample onto a Pixel 8 running Android 15 with a fresh user profile. The app sits in the foreground on the first-run voice screen, waiting for a model file that is not packaged yet. Then you tap download, switch to another app, and later revert the pull request that added the fetch. Those model weights are still sitting on disk even though git status is completely clean again.&lt;/p&gt;

&lt;p&gt;This is a proposed first-hour audit for a junior engineer joining a mobile AI repository, not a measured lab result. You should treat every download path as a lifecycle event that survives git revert, force-stop, and even uninstall in some cache layouts. The goal is simple: keep first-hour model weights out of shared storage after a rollback. Record the device, OS, framework versions, and one transition before anyone argues the device is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a rollback is not a cleanup
&lt;/h2&gt;

&lt;p&gt;A rollback restores source files, Gradle locks, and maybe a feature flag, but it does not restore the device. Android and iOS keep application caches, incomplete downloads, and media-store entries until you delete them on purpose. If the first PR wrote a &lt;code&gt;.bin&lt;/code&gt; or &lt;code&gt;.task&lt;/code&gt; file into Downloads, another app or a USB backup can still read it. Junior engineers often learn this only after a teammate restores a backup and finds a prototype speech model on the laptop.&lt;/p&gt;

&lt;p&gt;Industry demos this week keep pushing live voice and small companion models onto devices that look charming in a video. Your first hour on a real phone is less charming, because the file you fetched to unblock a demo becomes durable state. Git cannot see &lt;code&gt;cache/&lt;/code&gt;, &lt;code&gt;Files/&lt;/code&gt;, or an iCloud-eligible container, so a green CI check after revert is not evidence of removal. You need a device-side audit that starts before the first byte lands.&lt;/p&gt;

&lt;h3&gt;
  
  
  What usually survives the first revert
&lt;/h3&gt;

&lt;p&gt;Walk the filesystem with the same user profile you used for the download, not a second emulator that never ran the fetch. Then compare those paths against the rollback, because source control and storage have different lifetimes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incomplete HTTP bodies remain under &lt;code&gt;cache/&lt;/code&gt; when the user backgrounds the app during the first model download.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;DownloadManager&lt;/code&gt; or &lt;code&gt;URLSession&lt;/code&gt; temp file can be promoted into shared storage after a process kill.&lt;/li&gt;
&lt;li&gt;MediaStore or the Files app may index a &lt;code&gt;.tflite&lt;/code&gt; or &lt;code&gt;.bin&lt;/code&gt; you saved with a world-visible name.&lt;/li&gt;
&lt;li&gt;Uninstall on Android can keep the cache if the user enabled backup, then a reinstall quietly restores weights.&lt;/li&gt;
&lt;li&gt;iOS can include the file in an unencrypted Finder backup unless you excluded that directory from backup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those rows is a battery number or a device-class claim. They are storage outcomes you can confirm or reject on one phone with listed OS and app versions. If you cannot name the directory, you cannot claim the rollback removed the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the environment before anyone hits download
&lt;/h2&gt;

&lt;p&gt;Before any network call, write down device, OS, app state, permission state, and power state. You need those notes later when you argue that a rollback actually removed every on-device artifact. A junior PR that says “tested on my phone” without this block is not a mobile AI PR yet.&lt;/p&gt;

&lt;p&gt;Copy this block into the pull request and fill it on the device, not from memory after lunch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Proposed first-hour device card (fill on device, do not invent later)
Device:           Pixel 8 / iPhone 14  (pick the one in your hand)
OS:               Android 15 / iOS 18.x
App state:        cold start, first-run, foreground voice screen
Lifecycle event:  download starts -&amp;gt; Home / app switcher -&amp;gt; git revert
Network:          Wi-Fi only / cellular / offline after airplane mode
Power:            charging / battery saver / Low Power Mode
Permissions:      microphone, notifications, photos — granted or denied
Framework:        Android Gradle Plugin, Kotlin, Flutter, or RN version
Expected:         no model file in Downloads, Photos, Files, or backup
Limitation:       single user profile, no MDM, no work profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are not collecting a benchmark here. You are collecting enough context that a reviewer can repeat the same transition tomorrow. If the OS build or the permission state is missing, stop and fill the card before the download starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep hour-one iteration off the phone
&lt;/h2&gt;

&lt;p&gt;The fastest way to avoid leftover weights is to not download them during the first hour at all. Freeze the prompt contract, the JSON schema, and the timeout behavior against a remote development host, then package an on-device model only after that contract stops changing. Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;If your team already uses MonkeyCode, free model access and a free server option can host that first-hour contract off the device. The mobile client then sends the same request shape without baking a long-lived vendor key into the binary. You still must not store that host token in logcat, in plaintext &lt;code&gt;SharedPreferences&lt;/code&gt;, or in a committed &lt;code&gt;local.properties&lt;/code&gt; file. This article does not claim model names, quotas, hardware, uptime, or permanence for that hosted option.&lt;/p&gt;

&lt;p&gt;Use the remote host only as a contract gym. When the request and response shapes stabilize, you copy a frozen on-device package into an app-private cache that you can delete on rollback. That is a mobile packaging decision, not a server-operations guide, and it belongs in the first PR description.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed Android client: hour-one traffic talks to a revocable host.&lt;/span&gt;
&lt;span class="c1"&gt;// Do not ship this token in the APK. Inject it from CI or a local untracked file.&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;HourOneEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// development host, not production inference&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;             &lt;span class="c1"&gt;// memory only; never log the value&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;shouldDownloadOnDeviceModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contractFrozen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prAddsPackagedWeights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;contractFrozen&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;prAddsPackagedWeights&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;shouldDownloadOnDeviceModel&lt;/code&gt; is false, the first-hour build must not create a &lt;code&gt;DownloadManager&lt;/code&gt; request. Reviewers should reject a PR that fetches weights “just to see if generation works” on a shared engineering phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: a rollback storage audit you can run in one sitting
&lt;/h2&gt;

&lt;p&gt;The original artifact here is a decision table plus a pair of storage helpers. Run the table on one device after the lifecycle transition you wrote on the card. Label every cell as observed or not run; do not fill latency or battery fields you did not measure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First-hour action&lt;/th&gt;
&lt;th&gt;Where the file must live&lt;/th&gt;
&lt;th&gt;After &lt;code&gt;git revert&lt;/code&gt; you should still check&lt;/th&gt;
&lt;th&gt;Pass if&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-contract trial&lt;/td&gt;
&lt;td&gt;Remote development host only&lt;/td&gt;
&lt;td&gt;App logs and interceptors&lt;/td&gt;
&lt;td&gt;No weight file on device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaged on-device model in the PR&lt;/td&gt;
&lt;td&gt;App-private &lt;code&gt;cacheDir&lt;/code&gt; / Caches&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cache/&lt;/code&gt;, uninstall leftover, backup&lt;/td&gt;
&lt;td&gt;File gone or marked excluded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accidental &lt;code&gt;DownloadManager&lt;/code&gt; save&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Download/&lt;/code&gt; and MediaStore&lt;/td&gt;
&lt;td&gt;Zero indexed model files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backgrounded mid-download&lt;/td&gt;
&lt;td&gt;Incomplete temp in private cache&lt;/td&gt;
&lt;td&gt;Force-stop, then cold start&lt;/td&gt;
&lt;td&gt;Temp deleted, no retry into Downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback of the PR&lt;/td&gt;
&lt;td&gt;Source tree clean&lt;/td&gt;
&lt;td&gt;Same user profile filesystem&lt;/td&gt;
&lt;td&gt;No orphan &lt;code&gt;.bin&lt;/code&gt; / &lt;code&gt;.task&lt;/code&gt; / &lt;code&gt;.tflite&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Print the table in the PR with a third column filled from &lt;code&gt;adb&lt;/code&gt; or Finder, not from hope. A blank cell means the test was not run, which is honest and useful. A cell that claims “clean” without a path is not useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android: pin weights to private cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed helper. Treat as unexecuted sample until you run it on the device card.&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;modelCacheFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cacheDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"on-device-models"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mkdirs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"refusing path traversal in model name"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;assertNotShared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;downloads&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExternalStoragePublicDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DIRECTORY_DOWNLOADS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absolutePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;downloads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absolutePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"first-hour model escaped into Downloads"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absolutePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cacheDir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;absolutePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"first-hour model is not in app-private cache"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed audit after you background the app and revert the PR.&lt;/span&gt;
&lt;span class="c"&gt;# Replace the package name with the sample you actually installed.&lt;/span&gt;
adb shell am force-stop com.example.voiceapp
adb shell run-as com.example.voiceapp &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; cache/on-device-models &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;adb shell content query &lt;span class="nt"&gt;--uri&lt;/span&gt; content://media/external/file &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--projection&lt;/span&gt; _display_name:_data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--where&lt;/span&gt; &lt;span class="s2"&gt;"_display_name LIKE '%.tflite' OR _display_name LIKE '%.bin'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;content query&lt;/code&gt; prints a row, the rollback failed even when GitHub is green. Delete that row in a follow-up PR that only removes storage, and keep the original feature revert separate so bisect stays readable. Do not hide a storage cleanup inside a thirty-file refactor.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS: exclude caches from backup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed helper. Unexecuted until you run it on the iOS device listed on the card.&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;modelCacheURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;caches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cachesDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userDomainMask&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;caches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"on-device-models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withIntermediateDirectories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLResourceValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExcludedFromBackup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setResourceValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After revert, open Files, connect Finder, and search for the model filename on that same handset. A file that appears in an unencrypted backup is a first-hour leak, even if the app UI no longer offers generation. Cross-platform wrappers in Flutter or React Native need the same rule: native cache APIs, not &lt;code&gt;getApplicationDocumentsDirectory()&lt;/code&gt; dumped into a shareable folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed first-hour sequence
&lt;/h2&gt;

&lt;p&gt;Run this as a single-device experiment. Stop if any step needs a second phone or a production key.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fill the device card while the app is on the first-run voice screen in the foreground.&lt;/li&gt;
&lt;li&gt;Confirm the build has no &lt;code&gt;DownloadManager&lt;/code&gt; enqueue and no iOS &lt;code&gt;Downloads&lt;/code&gt; bookmark.&lt;/li&gt;
&lt;li&gt;Exercise the prompt contract against the revocable development host, then background the app for one minute.&lt;/li&gt;
&lt;li&gt;Force-stop the process, cold start, and confirm no model filename exists in shared storage.&lt;/li&gt;
&lt;li&gt;Merge nothing. Revert the local branch as if the first PR was rejected.&lt;/li&gt;
&lt;li&gt;Repeat the &lt;code&gt;adb&lt;/code&gt; or Finder search on the same user profile.&lt;/li&gt;
&lt;li&gt;Uninstall, reinstall the last main build, and search again for restored cache files.&lt;/li&gt;
&lt;li&gt;Write pass or fail per table row, including rows you skipped, with the OS version beside each row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Expected observation, if the helpers are wired: shared storage stays empty, and only an app-private cache could ever hold a packaged weight. Recovery outcome you want: after revert and uninstall, the filename is gone, not silently restored by backup. If the file reappears, the bug is backup eligibility, not the Git revert.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should not use this
&lt;/h2&gt;

&lt;p&gt;This workflow is for a junior engineer’s first hour, first PR, and first rollback on one personal or loaner handset. It is not a thermal study, not a tokens-per-second chart, and not a claim about every Android OEM’s backup implementation. Work profiles, MDM, and shared demo kiosks need extra policy that this article does not provide.&lt;/p&gt;

&lt;p&gt;Do not follow this approach if you are shipping a production speech feature this week and still lack a legal review of on-device artifacts. Do not use a public development host for prompts that contain user audio, medical text, or customer identifiers. Do not treat a free remote option as an offline guarantee; airplane mode still requires a packaged model and a separate radio-wake test.&lt;/p&gt;

&lt;p&gt;Skip the remote-host path when your repository already vendors a tiny on-device model and the first PR only changes UI copy. In that case you still run the storage audit, because a previous intern’s Downloads file may already be sitting on the loaner phone. The audit is the durable part; the host is optional scaffolding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to send back if you run it
&lt;/h2&gt;

&lt;p&gt;If you run this sequence, reply with device, OS, the exact transition, and whether the file recovered, restarted, or silently disappeared. Include the framework versions and whether backup was enabled, because those two fields change the uninstall row. Comparable evidence from one iPhone and one Pixel is worth more than a generic mobile UX thread with no paths.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Keep First-Hour Mic Buffers Out of Your Remote Debug Host</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:37:04 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-first-hour-mic-buffers-out-of-your-remote-debug-host-379k</link>
      <guid>https://dev.to/roronoa_/keep-first-hour-mic-buffers-out-of-your-remote-debug-host-379k</guid>
      <description>&lt;p&gt;You sit down with a Pixel 8 on Android 15, a fresh clone, and the sample voice screen already in the foreground. A teammate wants a first PR before lunch, and the recorder is mid-utterance when you press Home. Chat suggests a remote debug host so you can watch buffers arrive during that background transition. Those buffers are raw microphone PCM, so this first-hour choice is a privacy decision, not a convenience shortcut.&lt;/p&gt;

&lt;p&gt;This write-up is a proposed onboarding workflow for a junior joining a mobile AI repo, not a claimed lab report. Fill every version cell with values from the device in your hand. If a step is unexecuted, keep the notes labeled as a proposal rather than as measured behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure your first hour actually creates
&lt;/h2&gt;

&lt;p&gt;Junior onboarding on a mobile AI repo often starts with a mock inference URL because production keys stay locked. You need something that answers a transcribe route so the UI can move past the first recording. A public debug host looks like the fastest path when lunch is the unofficial deadline for your PR. The OS then backgrounds you, revokes the microphone, or sleeps the radio while an in-flight upload still completes.&lt;/p&gt;

&lt;p&gt;Your first PR has now taught the sample app to treat a convenience host as a voice sink. That failure differs from backup leakage and from platform speech recognition settings that belong in a different review. This one lives in first-hour debug topology: who may receive bytes while the voice session remains open. If reviewers only check UI copy, they will miss the host that actually received the PCM.&lt;/p&gt;

&lt;h3&gt;
  
  
  What must stay on the device during hour one
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Raw PCM or compressed microphone frames still sitting in the recorder&lt;/li&gt;
&lt;li&gt;Partial transcripts that exist only in memory and never reached a user-visible screen&lt;/li&gt;
&lt;li&gt;Device or install identifiers you attached as debug headers for “just this session”&lt;/li&gt;
&lt;li&gt;Staging or production inference base URLs copied from an internal wiki into a local extra&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those four leave the phone for a convenience host, you do not have a debug setup. You have an accidental data path that your rollback must close before anyone else clones the branch. Treat the first-hour host as part of the threat model, not as furniture around the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed environment, not a measured claim
&lt;/h2&gt;

&lt;p&gt;Treat the table as a form you complete on hardware you control. Do not paste these examples into a ship report unless the run actually happened.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example you should replace&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Pixel-class or iPhone-class hardware on your desk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;The Android or iOS build shown in Settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App state&lt;/td&gt;
&lt;td&gt;Voice screen foregrounded, one utterance in flight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;React Native, Flutter, or native, versions from the lockfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Wi-Fi only, cellular disabled before you tap Record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission&lt;/td&gt;
&lt;td&gt;Microphone granted, then revoked while the process is alive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;Unplugged; leave Battery Saver off unless that is the case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected&lt;/td&gt;
&lt;td&gt;No audio bytes on any remote host; local mock sees teardown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Record module versions from &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;pubspec.yaml&lt;/code&gt;, or Gradle before you file the PR. Guessing a framework version makes the next junior unable to replay your steps. Network and permission state belong in the PR body beside the screenshots, not in a later comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle experiment you can finish before the first PR
&lt;/h2&gt;

&lt;p&gt;This is a proposed test. Mark observations only after the commands run on your device.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install a &lt;strong&gt;local&lt;/strong&gt; mock on loopback, never a public URL, bound to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Point a debug-only build extra at that mock and keep the extra out of release manifests.&lt;/li&gt;
&lt;li&gt;Grant microphone, start recording, and speak one short phrase you will not reuse in tickets.&lt;/li&gt;
&lt;li&gt;Background the app with Home or Recents before the HTTP client reports completion.&lt;/li&gt;
&lt;li&gt;Revoke microphone from Settings while the process is still alive, then return to the app.&lt;/li&gt;
&lt;li&gt;Confirm whether the session recovered, restarted, or silently disappeared from the UI.&lt;/li&gt;
&lt;li&gt;Inspect the mock access log for bodies. Any media-like payload means the test failed.&lt;/li&gt;
&lt;li&gt;Repeat once with the radio off after the request starts, without adding a cloud fallback host.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Android: revoke the mic after you background the recorder
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed commands. Replace the package name with the sample app you cloned.&lt;/span&gt;
adb shell cmd appops &lt;span class="nb"&gt;set &lt;/span&gt;com.example.voiceai RECORD_AUDIO ignore
adb shell pm revoke com.example.voiceai android.permission.RECORD_AUDIO
adb shell dumpsys package com.example.voiceai | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A2&lt;/span&gt; RECORD_AUDIO
&lt;span class="c"&gt;# Do not relaunch from a debug intent that skips the normal activity stack.&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-s&lt;/span&gt; VoiceSession:D OkHttp:D AudioRecord:E | &lt;span class="nb"&gt;tee &lt;/span&gt;first-hour-mic.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a visible error on resume, not a silent retry against a second host. If logcat prints a &lt;code&gt;baseUrl&lt;/code&gt; that is not loopback, stop and revert the config change before you open the PR. Leaving that URL in &lt;code&gt;SharedPreferences&lt;/code&gt; is how a one-hour experiment becomes tomorrow’s production sink.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS analog, still labeled as a proposal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Simulator-only sketch. A physical iPhone run is required for the recording indicator.&lt;/span&gt;
xcrun simctl privacy booted revoke microphone com.example.voiceai
xcrun simctl terminate booted com.example.voiceai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a physical iPhone, use Settings → Privacy &amp;amp; Security → Microphone, then the app switcher. Confirm the orange recording indicator disappears and that no background &lt;code&gt;URLSession&lt;/code&gt; task keeps uploading. If the indicator lingers after revoke, your session object is still holding the audio unit and the first PR is not ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local mock that refuses audio bodies
&lt;/h2&gt;

&lt;p&gt;Keep the mock strict and small. It should answer health checks and reject anything that looks like media, including oversized JSON.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// local-voice-mock.mjs&lt;/span&gt;
&lt;span class="c1"&gt;// Proposed first-hour stub. Do not bind this port on a public interface.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8787&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;looksMedia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;octet-stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_BYTES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;looksMedia&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio_forbidden_on_debug_host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;REJECTED media-like body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local mock bound to 127.0.0.1:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node local-voice-mock.mjs
adb reverse tcp:8787 tcp:8787
&lt;span class="c"&gt;# The device can now call http://127.0.0.1:8787 without crossing the USB boundary.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire the client so backgrounding cancels the in-flight call. A quiet retry toward another host is the bug this first hour is supposed to catch, not a resilience feature you brag about in the PR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed React Native sketch. Confirm AppState behavior against your lockfile version.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NativeEventSubscription&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;attachVoiceTeardown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NativeEventSubscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;AppState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;postVoiceDebugMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;debug voice path must stay on loopback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;metadata-only; pcm must remain on device&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background_or_revoke_check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON body is intentional for hour one. You are not proving transcription quality against a golden set in this pass. You are proving that PCM never boarded a socket whose logs you do not control, including a “temporary” remote mock.&lt;/p&gt;

&lt;h2&gt;
  
  
  First PR shape and the rollback you rehearse before lunch
&lt;/h2&gt;

&lt;p&gt;Your first PR should change three things at most: the debug extra, the teardown listener, and a test that fails when a remote host is configured. If the diff needs a fourth production file, you have left first-hour scope and should split the work. Reviewers can reason about a loopback gate. They cannot reason about a surprise cloud fallback you added “so the demo still talks.”&lt;/p&gt;

&lt;h3&gt;
  
  
  PR checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Debug base URL is loopback-only in the sample target and compile-gated out of release.&lt;/li&gt;
&lt;li&gt;No public host lands in a committed &lt;code&gt;.env&lt;/code&gt;, even when your laptop already gitignores that file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adb reverse&lt;/code&gt; or the iOS loopback equivalent is written in the PR body as a required setup step.&lt;/li&gt;
&lt;li&gt;Background and microphone-revoke paths include expected UI copy, not only log snippets.&lt;/li&gt;
&lt;li&gt;Rollback is one revert plus uninstall, not a remote flag you cannot reach after hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rollback rehearsal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git revert &lt;span class="nt"&gt;--no-edit&lt;/span&gt; HEAD
&lt;span class="c"&gt;# Uninstall so a debug extra cannot survive in local storage across the revert.&lt;/span&gt;
adb uninstall com.example.voiceai
adb &lt;span class="nb"&gt;install &lt;/span&gt;app/build/outputs/apk/debug/app-debug.apk
adb logcat &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="c"&gt;# Cold-start the voice screen from the launcher, then grep once.&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"https?://"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After reinstall, open the voice screen once and watch for the old host. If it still appears, you stored it in &lt;code&gt;SharedPreferences&lt;/code&gt;, &lt;code&gt;UserDefaults&lt;/code&gt;, or an encrypted file you forgot to clear. Incomplete rollback is how a junior’s first afternoon becomes an incident for whoever merges next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where free model access and a free server belong
&lt;/h2&gt;

&lt;p&gt;You still need to draft the mock, the abort wiring, and the PR notes without pasting production audio into a chat window. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option that can generate the loopback stub and host a metadata-only contract check while you wait on staging credentials.&lt;/p&gt;

&lt;p&gt;Use that server for OpenAPI fragments, health checks, and CI jobs that never see microphone bytes. Do not point the Android or iOS client at it for live voice, even when the first-hour clock is running. If an assistant asks you to paste a failing request body, refuse and paste the status code plus headers only. If you want scaffolding, try those free models on the contract text in the repo rather than on a captured utterance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This workflow does not measure latency, energy use, or model quality, and it does not replace a privacy review. It also does not prove on-device inference; it only proves the debug path cannot become a voice sink. Teams with MDM, work profiles, or certificate pinning should follow their security group instead of a first-hour loopback stub.&lt;/p&gt;

&lt;p&gt;Skip this approach if you already have an approved internal mock that forbids media bodies. Skip it if your app must stream audio for a regulated clinical or financial workflow with a named vendor. Skip it if you cannot run on a physical device, because simulators lie about recording indicators and about USB reverse.&lt;/p&gt;

&lt;p&gt;Do not treat a public server as equivalent to loopback because both are labeled temporary. Temporary hosts still retain access logs after you close the laptop. Temporary juniors still open pull requests that other people will copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to report if you run the steps
&lt;/h2&gt;

&lt;p&gt;Reply with the device, OS build, framework versions from the lockfile, and the exact transition you used. Say whether the session recovered, restarted, or silently disappeared after microphone revoke. Include whether the mock log stayed empty of media and whether rollback required a preference wipe. Comparable environment evidence from one phone is more useful than a checklist emoji on the PR template.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Keep First-Hour Prompt Text Out of Keyboard Learning</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sun, 20 Sep 2026 10:42:46 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-first-hour-prompt-text-out-of-keyboard-learning-1b1c</link>
      <guid>https://dev.to/roronoa_/keep-first-hour-prompt-text-out-of-keyboard-learning-1b1c</guid>
      <description>&lt;p&gt;You join the mobile AI repo at 9:40 and the first ticket looks almost friendly. You add a multiline prompt field so testers can type a question into the on-device assistant. The app is still in the foreground on a development phone, and you have not touched Settings yet. Then you background the app, open Messages, and the keyboard suggests the private canary sentence you typed two minutes ago.&lt;/p&gt;

&lt;p&gt;That leak is not a model bug, and it will not show up in your unit tests. iOS Keyboard Learning and Android Autofill treat a prompt box like any other text field. Your first-hour PR can ship on-device inference and still hand the user's last utterance to the IME. Treat every procedure below as a proposed single-device experiment until you record your own outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually protecting
&lt;/h2&gt;

&lt;p&gt;You should treat the prompt field as sensitive even when inference never leaves the device. The OS mechanism that learns those characters sits outside your process and your unit tests. Your first PR should name the strings you refuse to feed to QuickType, Gboard, and Autofill.&lt;/p&gt;

&lt;p&gt;Keep these out of keyboard learning on the first-hour build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system or developer prompt templates pasted during debugging sessions&lt;/li&gt;
&lt;li&gt;user utterances that mention health, workplace, or account identifiers&lt;/li&gt;
&lt;li&gt;few-shot examples that still contain customer names from staging&lt;/li&gt;
&lt;li&gt;canary phrases you will later use to prove whether a leak happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are not hiding model weights in this pass, and you should not pretend otherwise. You are keeping first-hour text out of keyboard learning, Autofill stores, and OEM dictionaries. Those stores survive process death, and they can surface inside Messages after a single background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed test: one device, one lifecycle transition
&lt;/h2&gt;

&lt;p&gt;Label this work as a proposed test until you record device, OS, keyboard, and outcome. Do not copy another person's pass or fail and treat it as your release evidence. Open with one lifecycle transition: prompt focused, then Home gesture, then a different app. If you skip the Messages check, you will ship a green build that still leaks the canary.&lt;/p&gt;

&lt;p&gt;Record this environment before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;device model and OS version, plus whether this is a simulator, emulator, or hardware&lt;/li&gt;
&lt;li&gt;application state: cold start, prompt focused, draft not yet written to disk&lt;/li&gt;
&lt;li&gt;keyboard vendor and Autofill provider, including Gboard, Samsung Keyboard, or stock iOS&lt;/li&gt;
&lt;li&gt;permission state: microphone is optional here, because the leak is text-only&lt;/li&gt;
&lt;li&gt;framework versions: UIKit/SwiftUI, AndroidX, React Native, or Flutter, with exact numbers&lt;/li&gt;
&lt;li&gt;network and power: note them so you do not mix this failure with radio or thermal bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exact steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install a clean build and skip restoring a keyboard backup if the OS offers one.&lt;/li&gt;
&lt;li&gt;Focus the prompt field and type a unique canary such as &lt;code&gt;ZXQ-ONDEVICE-PROMPT-9182&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Submit once so the field blurs and the IME has a chance to commit the phrase.&lt;/li&gt;
&lt;li&gt;Background the app with the Home gesture; do not force-quit before the first check.&lt;/li&gt;
&lt;li&gt;Open Messages or Notes and type &lt;code&gt;ZXQ&lt;/code&gt; to see whether the canary is suggested.&lt;/li&gt;
&lt;li&gt;On Android, open Settings and inspect Autofill plus the keyboard personalization screen.&lt;/li&gt;
&lt;li&gt;Return to the app and confirm whether the field still shows the canary after resume.&lt;/li&gt;
&lt;li&gt;Kill the process, relaunch, and check the field, the IME bar, and any Autofill save prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Expected observations on a naive first PR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;iOS may offer the canary as a QuickType prediction after a single commit&lt;/li&gt;
&lt;li&gt;Android Autofill or Gboard may store the phrase against your application package&lt;/li&gt;
&lt;li&gt;process death may clear your ViewModel while the IME memory still keeps the canary&lt;/li&gt;
&lt;li&gt;reinstalling the app may not clear a cloud-backed keyboard dictionary on the same account&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Recovery outcome you want
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;the canary never appears in another app's keyboard suggestion bar&lt;/li&gt;
&lt;li&gt;clearing the field and killing the process leaves no local prompt draft&lt;/li&gt;
&lt;li&gt;testers do not need a keyboard dictionary reset after every internal build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the canary silently appears in another app, the first PR failed the privacy bar. On-device inference quality does not excuse an IME side channel on a first-hour build. Write the failed observation into the PR, including keyboard vendor and OS point release.&lt;/p&gt;

&lt;h2&gt;
  
  
  First-hour code you should put in the PR
&lt;/h2&gt;

&lt;p&gt;Treat the snippets below as starting points you still have to execute on a real device. These flags are not a guarantee against OEM keyboards, work profiles, or accessibility services. Check the framework version in the PR body so reviewers can reproduce the same traits. If a flag is missing on your min OS, document the gap instead of commenting it out quietly.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS UIKit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;PromptViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;promptView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UITextView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autocorrectionType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spellCheckingType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smartQuotesType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smartDashesType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smartInsertDeleteType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;autocapitalizationType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;none&lt;/span&gt;
        &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;none&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;#available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt; &lt;span class="mf"&gt;17.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inlinePredictionType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;#available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt; &lt;span class="mf"&gt;18.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;promptView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writingToolsBehavior&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;none&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read Apple's UITextInputTraits notes before you assume one property disables every learning path. Writing Tools and inline predictions are newer OS mechanisms than the classic autocorrect toggle. Gate the newer properties with availability checks so the first PR still compiles on older targets. Re-run the canary after each OS bump, because keyboard subsystems change in point releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  SwiftUI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;TextEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textInputAutocapitalization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;never&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;autocorrectionDisabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textContentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;privacySensitive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The privacySensitive modifier can hide text in system screen captures and some app switcher thumbnails. It does not replace autocorrect, text content type, or inline prediction controls on the field. Keep both layers in the first PR if your testers use TestFlight screenshots during onboarding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android XML and view code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;EditText&lt;/span&gt;
    &lt;span class="na"&gt;android:id=&lt;/span&gt;&lt;span class="s"&gt;"@+id/prompt"&lt;/span&gt;
    &lt;span class="na"&gt;android:inputType=&lt;/span&gt;&lt;span class="s"&gt;"textMultiLine|textNoSuggestions"&lt;/span&gt;
    &lt;span class="na"&gt;android:importantForAutofill=&lt;/span&gt;&lt;span class="s"&gt;"noExcludeDescendants"&lt;/span&gt;
    &lt;span class="na"&gt;android:importantForContentCapture=&lt;/span&gt;&lt;span class="s"&gt;"noExcludeDescendants"&lt;/span&gt;
    &lt;span class="na"&gt;android:autofillHints=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="na"&gt;android:imeOptions=&lt;/span&gt;&lt;span class="s"&gt;"flagNoPersonalizedLearning"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;promptEditText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setImportantForAutofill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SDK_INT&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_CODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;promptEditText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setImportantForContentCapture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;promptEditText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAutofillHints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;flagNoPersonalizedLearning is an IME option, and a vendor keyboard may ignore it completely. You need importantForAutofill and importantForContentCapture because they close two different capture channels. Empty autofill hints are not enough if the view still reports itself as important for Autofill. After the XML change, still set the same flags in code so later Compose wrappers cannot drift.&lt;/p&gt;

&lt;p&gt;Call the Autofill APIs after setContentView so the view tree does not restore a naive default. Content capture arrived later than Autofill, so wrap it with an SDK_INT check in the first PR. Null autofill hints plus IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS is the conservative pairing for prompts. Log the sdkInt and the flag values once in debug, then delete that log before you ship.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jetpack Compose
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;OutlinedTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;keyboardOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;autoCorrectEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;capitalization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardCapitalization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;keyboardType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose keyboard options do not automatically disable Autofill on every BOM version you might pin. Check the AndroidX release notes for your BOM and add Autofill semantics if the canary still appears. Keep the XML flags in mind when this field is hosted inside a hybrid View/Compose screen. Hybrid trees are a common first-PR miss when a junior only patches the Compose widget.&lt;/p&gt;

&lt;h3&gt;
  
  
  React Native
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TextInput&lt;/span&gt;
  &lt;span class="na"&gt;multiline&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onChangeText&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setPrompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;autoCorrect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;autoCapitalize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;
  &lt;span class="na"&gt;spellCheck&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;textContentType&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt;
  &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
  &lt;span class="na"&gt;importantForAutofill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"no"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the React Native version in your first-hour notes, because autoComplete mapping changed across releases. textContentType none is an iOS prop, and importantForAutofill is the Android counterpart on TextInput. spellCheck false does not disable Gboard personalization by itself on every Android device. If you wrap TextInput, forward these props or the first PR will look correct and still leak.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flutter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;controller:&lt;/span&gt; &lt;span class="n"&gt;promptController&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;autocorrect:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;enableSuggestions:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;enableIMEPersonalizedLearning:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;spellCheckConfiguration:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SpellCheckConfiguration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nl"&gt;keyboardType:&lt;/span&gt; &lt;span class="n"&gt;TextInputType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;textCapitalization:&lt;/span&gt; &lt;span class="n"&gt;TextCapitalization&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;none&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;enableIMEPersonalizedLearning false is the Android Flutter flag that juniors miss during onboarding. Put it on the checklist beside model packaging, because it is unrelated to inference quality. SpellCheckConfiguration.disabled stops the OS spell service from taking a second copy of the buffer. Run the canary on a physical Pixel or Samsung phone, not only on the desktop emulator keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands that help you prove the leak
&lt;/h2&gt;

&lt;p&gt;You cannot dump another vendor's keyboard database on a stock phone without extra privileges. You can still collect evidence around your process, your view flags, and accidental log copies. If the canary appears in logcat, fix logging in the same PR as the IME flags. Keyboard learning is not the only first-hour side channel on a debug mobile AI build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android: inspect Autofill around your prompt view after typing the canary&lt;/span&gt;
adb shell dumpsys autofill | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 24 &lt;span class="s2"&gt;"prompt"&lt;/span&gt;

&lt;span class="c"&gt;# Android: fail the PR if debug logging echoed the canary&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"ZXQ-ONDEVICE-PROMPT-9182"&lt;/span&gt;

&lt;span class="c"&gt;# Android: confirm the focused view after you tap the field&lt;/span&gt;
adb shell dumpsys window windows | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 12 &lt;span class="s2"&gt;"mCurrentFocus"&lt;/span&gt;

&lt;span class="c"&gt;# iOS Simulator: identify the booted device before you reset test state&lt;/span&gt;
xcrun simctl list devices booted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a physical iPhone, Reset Keyboard Dictionary is the recovery control after a failed canary. Document that reset because it is destructive for the tester's other learned words and shortcuts. On Android, a keyboard app reset in Settings is similarly destructive and should be a last resort. Prefer preventing the learn step over asking testers to wipe dictionaries after every build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table for the first PR
&lt;/h2&gt;

&lt;p&gt;Use this table during the first PR review, not as a substitute for the canary run. If the product needs a draft after process death, store it in the app container only. File protection and encrypted storage are a different change from IME flags, so split the diffs. Do not copy prompt flags onto a public help search box or you will harm everyday typing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field purpose&lt;/th&gt;
&lt;th&gt;Disable IME learning&lt;/th&gt;
&lt;th&gt;Disable Autofill&lt;/th&gt;
&lt;th&gt;Keep a local draft&lt;/th&gt;
&lt;th&gt;First-PR note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On-device user prompt&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Memory only unless policy requires a draft&lt;/td&gt;
&lt;td&gt;Default for this article&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public help search&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Usually no&lt;/td&gt;
&lt;td&gt;Disk cache is fine&lt;/td&gt;
&lt;td&gt;Do not reuse prompt flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API key or session paste&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Use a secure field, not a prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal few-shot editor&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Encrypted app storage only&lt;/td&gt;
&lt;td&gt;Still run the canary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If reviewers ask for a surviving draft, that is a storage ticket, not an IME ticket. Mixing those concerns is how first-hour PRs grow into unreviewable diffs. Keep the canary, the flags, and the logging redaction in one privacy-sized change. Leave model packaging and rollback rehearsal to the PRs that already own those failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a coding assistant actually helps
&lt;/h2&gt;

&lt;p&gt;A junior engineer can waste the first hour hunting flag names across UIKit, Jetpack, React Native, and Flutter. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source project with free model access and a free server option you can use to generate platform wrappers from the table above. You still owe the device run, because generated flags are not evidence of keyboard behavior.&lt;/p&gt;

&lt;p&gt;Paste your real prompt widget and ask only for the platform-specific props that match this table. Then run the canary steps on one phone and record whether the suggestion recovered, restarted, or silently appeared. Skip any generated claim about battery, quotas, or model quality, because this leak is an OS input path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not use this
&lt;/h2&gt;

&lt;p&gt;This approach does not stop a user from pasting the prompt into Messages themselves on purpose. It does not control a work profile keyboard, a Bluetooth hardware keyboard, or an accessibility service. It also does not replace screenshot policy, backup exclusion, or redaction of prompt text in crash logs. Treat IME flags as a product hygiene control, not as a compliance certification for regulated data.&lt;/p&gt;

&lt;p&gt;You should not disable learning on every text box, because navigation search becomes slower and more error prone. You should not skip the canary test because a simulator keyboard looked clean during the first hour. You should not ship FLAG_SECURE on every screen only to hide this leak; that is a different tradeoff. Health and finance clients still need legal review beyond a TextField configuration in the onboarding PR.&lt;/p&gt;

&lt;p&gt;Inline predictions, Writing Tools, content capture, and OEM clipboard suggestions change by OS point release. Re-run the steps when you bump minSdk or the iOS deployment target. Flutter's IME personalization flag is an Android control and will not save you on iOS. Cross-platform wrappers still need the native props on each side of the bridge.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should send back
&lt;/h2&gt;

&lt;p&gt;Please do not reply with a generic claim that the field looked fine on your machine. Send comparable evidence: device, OS, keyboard vendor, Autofill provider, and the lifecycle transition. Include whether the canary recovered after process death, restarted after reinstall, or silently appeared elsewhere. If the canary stayed inside your process, the first PR is ready for a reviewer to reproduce.&lt;/p&gt;

&lt;p&gt;If it escaped into the keyboard bar, fix the field before you debate on-device model quality. First-hour mobile AI work fails in OS input paths more often than it fails in the interpreter. Your onboarding checklist should include this canary beside permission revoke tests and backup exclusion checks.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Keep Your First-Hour AI Host Out of Background Resume</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sat, 19 Sep 2026 08:58:48 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-your-first-hour-ai-host-out-of-background-resume-3al9</link>
      <guid>https://dev.to/roronoa_/keep-your-first-hour-ai-host-out-of-background-resume-3al9</guid>
      <description>&lt;p&gt;You sit down with a loaner phone, a fresh clone, and a ticket that says get inference working locally. The debug build sits in the foreground, microphone permission is already granted, and the radio is ordinary office Wi-Fi. You paste a laptop prototype host into a gitignored config because production still needs a key you lack. Then you switch apps to read the README, and that single background transition is where first-hour AI PRs start leaking hosts, prompts, and retries.&lt;/p&gt;

&lt;p&gt;This write-up is a proposed onboarding workflow, not a lab report from one named handset. You should treat every command as homework to run on hardware you control and then record. If a step cannot be reproduced on a single phone, it does not belong in your first pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the first hour ships the wrong host
&lt;/h2&gt;

&lt;p&gt;Junior engineers joining a mobile AI repo usually fail in the same three places before lunch. The prototype URL is compiled into a flavor that QA will sideload later in the week. The prompt is printed to logcat or the Xcode console so the first completion looks debuggable. The client then retries the cloud call from &lt;code&gt;onResume&lt;/code&gt; or &lt;code&gt;scenePhase == .active&lt;/code&gt;, as if every foregrounding were a new chat turn.&lt;/p&gt;

&lt;p&gt;None of those mistakes require a large model, a fancy SDK, or a production account. They appear because the first hour rewards a green spinner more than a lifecycle check. You can still point a debug build at a desk-side server. You cannot let that server become the default target after the user leaves the app.&lt;/p&gt;

&lt;p&gt;Cross-platform wrappers make the same resume mistake with extra listeners you will not see in the activity file. Flutter plugins and React Native networking stacks can hold a JS or Dart queue that survives an Android pause. Your first PR has to name those listeners, or the gate you add in Kotlin will not be the gate that actually runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  First-hour isolation checklist
&lt;/h2&gt;

&lt;p&gt;Work through this list before you open the pull request, and paste the results into the description.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the prototype host in a gitignored file, never in &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, &lt;code&gt;Info.plist&lt;/code&gt;, or a committed &lt;code&gt;.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Gate the host with a debug-only build flag so release flavors cannot resolve it at all.&lt;/li&gt;
&lt;li&gt;Refuse to log raw prompts, transcripts, tokens, or authorization headers at any level.&lt;/li&gt;
&lt;li&gt;Do not enqueue inference from resume, start, or &lt;code&gt;applicationDidBecomeActive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Record microphone and network permission state before and after a revoke test.&lt;/li&gt;
&lt;li&gt;Write the rollback command in the PR so a reviewer can revert without asking you.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Android: local host, not a shipped constant
&lt;/h3&gt;

&lt;p&gt;Keep desk addresses in &lt;code&gt;local.properties&lt;/code&gt;, which Gradle already knows how to ignore in a normal Android tree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# local.properties — gitignored
&lt;/span&gt;&lt;span class="py"&gt;ai.prototype.host&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://192.168.1.23:8080&lt;/span&gt;
&lt;span class="py"&gt;ai.prototype.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire those values only into the debug &lt;code&gt;BuildConfig&lt;/code&gt; block, and force the release flavor to empty strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// android/app/build.gradle.kts&lt;/span&gt;
&lt;span class="nf"&gt;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;buildTypes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"debug"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;buildConfigField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"AI_PROTOTYPE_HOST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"\"${project.findProperty("&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="s"&gt;") ?: ""}\""&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;buildConfigField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"AI_PROTOTYPE_ENABLED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"${project.findProperty("&lt;/span&gt;&lt;span class="n"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="s"&gt;") ?: "&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="s"&gt;"}"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;getByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;buildConfigField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"AI_PROTOTYPE_HOST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"\"\""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;buildConfigField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"AI_PROTOTYPE_ENABLED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make resume a no-op whenever the desk host is in play. Production code may refresh a session token, but it must not replay the last utterance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InferenceResumeGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isPrototypeEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onForeground&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isPrototypeEnabled&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isBlank&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Proposed behavior: never retry a desk host after backgrounding.&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// Production path may refresh a token. It must not replay the last prompt.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  iOS: xcconfig instead of a hardcoded URL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Debug.xcconfig — do not commit a real desk IP
AI_PROTOTYPE_HOST = http:/$()/192.168.1.23:8080
AI_PROTOTYPE_ENABLED = YES
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;scenePhaseDidChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;ScenePhase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kt"&gt;BuildFlags&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prototypeEnabled&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Proposed: skip prompt replay. Token refresh belongs in a dedicated session type.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A single-device test you can paste into the PR
&lt;/h2&gt;

&lt;p&gt;Treat the following as a proposed experiment. Fill in your device, OS, and framework versions before you claim any result. Do not invent battery figures or assume a whole device class behaves the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment to record
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Device and OS, written as model plus version, not as "a modern Android phone"&lt;/li&gt;
&lt;li&gt;App state at start: foreground, microphone allowed, Wi-Fi associated, charger optional&lt;/li&gt;
&lt;li&gt;Framework and HTTP stack: native, React Native, or Flutter, plus client library version&lt;/li&gt;
&lt;li&gt;Transition under test: app switcher or Home for thirty seconds, then resume&lt;/li&gt;
&lt;li&gt;Second transition: Settings revoke of microphone, resume, then an attempted utterance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exact steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Launch the debug build and send one short prompt against the prototype host only.&lt;/li&gt;
&lt;li&gt;Confirm that host appears in memory or gitignored config, not in the log buffer.&lt;/li&gt;
&lt;li&gt;Background the app for thirty seconds without swiping the process out of recents.&lt;/li&gt;
&lt;li&gt;Resume and watch whether a second HTTP call leaves the device toward the desk.&lt;/li&gt;
&lt;li&gt;Revoke microphone permission, resume again, and try to speak a follow-up prompt.&lt;/li&gt;
&lt;li&gt;Write down whether inference recovered, restarted from scratch, or silently disappeared.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Commands worth running
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android: watch for the prototype host and prompt text&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | rg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"192&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;168&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;|Authorization:|prompt|transcript"&lt;/span&gt;

&lt;span class="c"&gt;# Android: revoke and restore microphone without reinstalling&lt;/span&gt;
adb shell pm revoke com.example.app android.permission.RECORD_AUDIO
adb shell pm grant com.example.app android.permission.RECORD_AUDIO

&lt;span class="c"&gt;# Android: confirm resume did not quietly restart the process as a fake fix&lt;/span&gt;
adb shell dumpsys activity processes | rg com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# iOS (proposed): stream unified logs without printing utterance text&lt;/span&gt;
log stream &lt;span class="nt"&gt;--predicate&lt;/span&gt; &lt;span class="s1"&gt;'subsystem CONTAINS "com.example.app"'&lt;/span&gt; &lt;span class="nt"&gt;--style&lt;/span&gt; compact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Expected observations, not invented numbers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;After backgrounding, you should see zero new requests to the desk host.&lt;/li&gt;
&lt;li&gt;After microphone revoke, the UI should explain the missing permission instead of retrying audio.&lt;/li&gt;
&lt;li&gt;After restore, the session may restart, but it must not replay the previous utterance automatically.&lt;/li&gt;
&lt;li&gt;Radio and energy claims stay qualitative here: if the radio wakes for the desk host, the resume gate failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your first hour needs a desk-side model so the PR does not embed a production key, MonkeyCode offers free model access and a free server option you can aim at that gitignored host. Disclosure: This article was prepared as part of MonkeyCode's product outreach. That path stays honest only while it remains off release flavors, off resume retries, and off shared logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table for the first PR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Ship in the first PR?&lt;/th&gt;
&lt;th&gt;Rollback if it leaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gitignored debug host, resume gated&lt;/td&gt;
&lt;td&gt;Yes, with the test notes attached&lt;/td&gt;
&lt;td&gt;Delete local config, then clean rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host string in a committed sample&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git revert&lt;/code&gt;, then rotate the host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt or transcript in logs&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Wipe log buffers and strip the logger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry on every &lt;code&gt;onResume&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Gate the client and attach the test above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release flavor resolves the desk IP&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Flip &lt;code&gt;BuildConfig&lt;/code&gt; and cut a new build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microphone missing, call still fires&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Fail closed and show a permission screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  First PR, then the first rollback
&lt;/h2&gt;

&lt;p&gt;Your first pull request should carry three artifacts besides the feature diff itself. Include the filled environment table, the log command you actually ran, and a rollback snippet a reviewer can execute without a Slack thread. Reviewers are not mind readers, and onboarding tickets die when the only recovery plan is "we will hotfix later."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed rollback for a leaked prototype host&lt;/span&gt;
git revert &lt;span class="nt"&gt;--no-edit&lt;/span&gt; HEAD
rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"192&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.168&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.|AI_PROTOTYPE_HOST|prototype.host"&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;span class="c"&gt;# If a shared desk server saw real utterances, rotate that endpoint before the next install.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not treat rollback as a follow-up chore for week two. The onboarding ticket is unfinished until a second engineer can remove the host and still compile a debug flavor. If the leaked URL was reachable from more than your laptop, rotating it is part of the same PR, not a private note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This workflow does not measure milliwatts, does not name a neural net, and does not claim any cloud path will remain free. OS vendors change background rules between minor releases, and a plugin may register resume hooks your activity-level gate never sees. Emulators often skip radio, permission sheets, and thermal throttling that production phones will hit during the same thirty-second background window.&lt;/p&gt;

&lt;p&gt;Skip this approach if you handle real user audio on day one, if legal forbids any desk-side prompt, or if you cannot run a physical device. Do not use a prototype host as a silent fallback when on-device inference fails either. That is a product decision, and it belongs in a later PR with a user-visible path, a permission story, and a rollback that QA can actually rehearse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to report back
&lt;/h2&gt;

&lt;p&gt;If you run the resume test, comment with device, OS, and the exact transition you used. Say whether the prototype call recovered, restarted, or silently disappeared after you returned to the app. Comparable notes from one phone beat a generic claim that backgrounding already works.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Keep On-Device Transcripts Out of Phone Backup on Your First Mobile AI PR</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:01:17 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-on-device-transcripts-out-of-phone-backup-on-your-first-mobile-ai-pr-45hb</link>
      <guid>https://dev.to/roronoa_/keep-on-device-transcripts-out-of-phone-backup-on-your-first-mobile-ai-pr-45hb</guid>
      <description>&lt;p&gt;You sit down with a fresh clone and a first ticket that sounds harmless on Slack. The ticket asks you to persist the last voice session so offline replay still works after the process dies. Nobody mentions backup, restore, or what happens when a reviewer asks you to roll the PR back. The trap is not the schema; it is whether that file rides into iCloud or Google backup after you merge.&lt;/p&gt;

&lt;p&gt;This is a proposed first-hour workflow for a junior engineer joining a mobile AI repo, not a scored lab report. You will treat backup exclusion as a merge gate, then rehearse restore after rollback. Record the device, OS, framework, permission state, and network condition before you claim anything recovered, restarted, or silently disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first-hour failure mode nobody put in the ticket
&lt;/h2&gt;

&lt;p&gt;On-device voice and offline replay almost always grow a local store during the first PR. That store holds utterances, partial captions, and sometimes speaker labels that never belonged in a cloud sync channel. Phone backup is an OS mechanism, not your product sync, and it can copy those files while the app is backgrounded or even uninstalled later. If your first cache lands in Documents, shared storage, or a default database path, a restore can resurrect user speech after you thought the rollback was clean.&lt;/p&gt;

&lt;p&gt;You should assume AI-drafted persistence will pick the convenient directory unless you constrain it. Convenient directories are the ones backup systems already know how to upload. Your job in hour one is not to debate schema elegance. Your job is to prove the transcript file is excluded, then prove a restore cannot bring it back after revert.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need to record before you touch files
&lt;/h2&gt;

&lt;p&gt;Fill this block in the PR description before you paste commands. Do not invent numbers; leave blanks until a device actually answers you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device and OS: for example Pixel 8a / Android 15, or iPhone 14 / iOS 18.6&lt;/li&gt;
&lt;li&gt;App state: fresh install, logged-out, microphone permission denied or granted&lt;/li&gt;
&lt;li&gt;Framework: native, React Native, or Flutter, plus the exact dependency versions&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi, cellular, or airplane mode, because some backup transports wait for unmetered links&lt;/li&gt;
&lt;li&gt;Power: charging or battery saver, because backup daemons defer work under energy pressure&lt;/li&gt;
&lt;li&gt;Expected observation: the transcript file exists for offline replay, but it is absent from backup contents&lt;/li&gt;
&lt;li&gt;Recovery outcome after rollback: the file is gone, and a restored backup does not recreate it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you cannot name the OS backup transport, you are not ready to merge the cache. Write that limitation in the PR instead of calling the feature offline-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proposed backup-and-restore experiment
&lt;/h2&gt;

&lt;p&gt;Label this as an unexecuted checklist until you run it on one physical device. Do not copy a lifecycle matrix from another PR and call it coverage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install a debug build that writes one short transcript after a single offline session.&lt;/li&gt;
&lt;li&gt;Confirm the file path with the commands in the inspection section below.&lt;/li&gt;
&lt;li&gt;Trigger backup with the platform tool for that OS, then inspect the backup contents or exclusion flags.&lt;/li&gt;
&lt;li&gt;Revert the PR or uninstall the build, then restore the backup onto the same device.&lt;/li&gt;
&lt;li&gt;Open the app cold and check whether utterances reappear without a new recording.&lt;/li&gt;
&lt;li&gt;Grant and revoke microphone permission once, then repeat backup, because some queues rewrite the store on denial.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You are looking for three outcomes only: recovered, restarted empty, or silently disappeared. Anything else belongs in limitations, not in a pass comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exclude the transcript store on iOS
&lt;/h2&gt;

&lt;p&gt;Keep the SQLite file in Application Support, not in Documents, then set the exclusion flag on the file URL. Documents is user data from the system's point of view, which makes it a backup candidate even when you consider it a cache. Caches can be purged under storage pressure, so it is the wrong place for replay that must survive a cold start. Application Support plus an explicit exclusion is the usual honest compromise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;

&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;TranscriptStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applicationSupportDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userDomainMask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;appropriateFor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"voice-replay"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withIntermediateDirectories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transcripts.sqlite"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;excludeFromBackup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLResourceValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExcludedFromBackup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setResourceValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;excludeFromBackup()&lt;/code&gt; after the first successful write, then read the flag back. A write that never sets the resource value will look fine in the simulator until someone restores a laptop backup. Proposed check: if &lt;code&gt;isExcludedFromBackup&lt;/code&gt; is not &lt;code&gt;true&lt;/code&gt;, fail the debug launch, not just a log line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;assertExcludedFromBackup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resourceValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;forKeys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExcludedFromBackupKey&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;precondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExcludedFromBackup&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"transcript store is backup-eligible"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Exclude the transcript store on Android
&lt;/h2&gt;

&lt;p&gt;Android Auto Backup and device-to-device migration read your manifest, not your code comments. A default &lt;code&gt;android:allowBackup="true"&lt;/code&gt; with no exclusion rules will pack app databases when Google backup runs. Android 12 and later also honor &lt;code&gt;dataExtractionRules&lt;/code&gt; for cloud backup versus device transfer, so you should ship both the older and newer XML. Put transcripts in internal storage, never in shared storage, and name the file so a reviewer can grep it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- AndroidManifest.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;application&lt;/span&gt;
    &lt;span class="na"&gt;android:allowBackup=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
    &lt;span class="na"&gt;android:fullBackupContent=&lt;/span&gt;&lt;span class="s"&gt;"@xml/backup_rules"&lt;/span&gt;
    &lt;span class="na"&gt;android:dataExtractionRules=&lt;/span&gt;&lt;span class="s"&gt;"@xml/data_extraction_rules"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- res/xml/backup_rules.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;full-backup-content&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"database"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"transcripts.db"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"voice-replay/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/full-backup-content&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- res/xml/data_extraction_rules.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;data-extraction-rules&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;cloud-backup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"database"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"transcripts.db"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"voice-replay/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/cloud-backup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;device-transfer&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"database"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"transcripts.db"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclude&lt;/span&gt; &lt;span class="na"&gt;domain=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"voice-replay/"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/device-transfer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/data-extraction-rules&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the product must restore settings across phones, exclude only the utterance store, not the entire app. A blanket &lt;code&gt;allowBackup="false"&lt;/code&gt; hides the problem from review and breaks legitimate preference migration. Call that tradeoff out in the PR so QA does not treat a missing backup as a privacy win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-platform gotchas in Flutter and React Native
&lt;/h2&gt;

&lt;p&gt;Flutter's &lt;code&gt;getApplicationDocumentsDirectory()&lt;/code&gt; maps to backup-eligible locations on both platforms more often than juniors expect. Prefer application-support or internal files, then still set the native exclusion because Dart cannot see iCloud flags by itself. React Native file helpers have the same split: a path that works offline is not automatically excluded from Auto Backup. Add a tiny native module or config plugin that sets the iOS resource value and ships the Android XML, then fail CI if those files are missing from the merged artifacts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed Flutter check, not a measured benchmark.&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;getApplicationSupportDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${support.path}&lt;/span&gt;&lt;span class="s"&gt;/voice-replay/transcripts.sqlite'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;recursive:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAsBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;flush:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Call a MethodChannel that sets isExcludedFromBackup / verifies XML merge.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not let the first PR store transcripts beside model shards or logs. Mixed directories make exclusion rules brittle, and the next engineer will add a screenshot cache that inherits the wrong policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands you can run before the PR
&lt;/h2&gt;

&lt;p&gt;These commands inspect state. They are not exploits, and they will not print a pass unless you read the output against the expected path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS Simulator, proposed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Record the simulator OS version in the PR.&lt;/span&gt;
xcrun simctl list devices booted
&lt;span class="nv"&gt;APP_DATA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;xcrun simctl get_app_container booted com.example.voiceapp data&lt;span class="si"&gt;)&lt;/span&gt;
find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DATA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'transcripts.sqlite'&lt;/span&gt;
xattr &lt;span class="nt"&gt;-p&lt;/span&gt; com.apple.MobileBackup &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_DATA&lt;/span&gt;&lt;span class="s2"&gt;/Library/Application Support/voice-replay/transcripts.sqlite"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A value of &lt;code&gt;1&lt;/code&gt; on &lt;code&gt;com.apple.MobileBackup&lt;/code&gt; means excluded. Missing xattr after a write means your Swift flag never stuck. Re-run after a simulated restore if your Xcode version exposes that flow; otherwise use a device and say so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android debug build, proposed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell getprop ro.build.version.release
adb shell dumpsys package com.example.voiceapp | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A4&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; backup
adb shell run-as com.example.voiceapp &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; files/voice-replay databases
&lt;span class="c"&gt;# Optional: request an immediate backup, then confirm the transport did not list transcripts.db&lt;/span&gt;
adb shell bmgr backupnow com.example.voiceapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;run-as&lt;/code&gt; fails, you are not on a debug install and you should stop. Do not switch to world-readable storage just to make listing easier. Paste the command output into the PR so reviewers can see the path and the backup flags together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback is not uninstall
&lt;/h2&gt;

&lt;p&gt;Your first rollback rehearsal should restore a backup taken while the bad cache still existed. Uninstalling the debug build only proves the package is gone, not that the OS forgot the utterances. A teammate who later restores the phone for a device swap can resurrect speech that your revert never deleted from backup storage.&lt;/p&gt;

&lt;p&gt;Walk this sequence on one device and write the outcome in one sentence.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Merge candidate writes &lt;code&gt;transcripts.sqlite&lt;/code&gt; or &lt;code&gt;transcripts.db&lt;/code&gt; during an offline session.&lt;/li&gt;
&lt;li&gt;Backup completes while the file still exists.&lt;/li&gt;
&lt;li&gt;You revert the PR, or you install a build that no longer ships the cache code.&lt;/li&gt;
&lt;li&gt;You restore the backup taken in step 2.&lt;/li&gt;
&lt;li&gt;Cold start: the transcript must not reappear, and the exclusion must still apply if the file is recreated empty.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the restored app shows the old utterance, the first PR is not rolled back in any user-visible sense. Fix exclusion, wipe the local file on version downgrade, and repeat the restore. Do not call that wipe a privacy feature unless you also proved backup no longer contains the bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drafting helpers with free model access
&lt;/h2&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. A junior can use MonkeyCode's free model access and free server option to draft the exclusion helper, the XML stubs, or a PR checklist from this article. That is a reasonable way to get a first patch onto the branch without waiting on a paid seat. It does not replace the device commands, and it does not prove backup exclusion by itself.&lt;/p&gt;

&lt;p&gt;Ask the model for the resource-value call and the two Android XML files, then you run the inspection on hardware. If the draft points at Documents, shared storage, or &lt;code&gt;allowBackup="false"&lt;/code&gt; with no explanation, reject it the same way you would reject a hand-written patch. Keep the chat log out of the PR; attach the &lt;code&gt;xattr&lt;/code&gt;, &lt;code&gt;dumpsys&lt;/code&gt;, and &lt;code&gt;bmgr&lt;/code&gt; output instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First-PR choice&lt;/th&gt;
&lt;th&gt;Offline replay after kill&lt;/th&gt;
&lt;th&gt;Likely backup behavior&lt;/th&gt;
&lt;th&gt;Rollback after restore&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Documents or default app database, no exclusion&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Transcripts often included&lt;/td&gt;
&lt;td&gt;Old utterances can return&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caches directory only&lt;/td&gt;
&lt;td&gt;May vanish under storage pressure&lt;/td&gt;
&lt;td&gt;Sometimes skipped, not guaranteed&lt;/td&gt;
&lt;td&gt;Replay randomly dies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Support or internal files, exclusion set, XML merged&lt;/td&gt;
&lt;td&gt;Works if you re-open the store&lt;/td&gt;
&lt;td&gt;Expected exclude if flags stick&lt;/td&gt;
&lt;td&gt;Restore should not resurrect speech&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;allowBackup="false"&lt;/code&gt; for the whole app&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;Settings and honest data also skipped&lt;/td&gt;
&lt;td&gt;Hides the review signal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the third row unless product, legal, or accessibility requires cross-device transcript restore. If you do require restore, encrypt at rest, document the transport, and stop calling the feature on-device-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not ship this
&lt;/h2&gt;

&lt;p&gt;This workflow does not measure backup bytes, radio wake, or battery drain, and you should not invent those figures. Exclusion flags can lag across OS versions, work profiles, and manufacturer backup apps that ignore Google’s XML. Simulator xattr checks are weaker than a device restore, and &lt;code&gt;bmgr&lt;/code&gt; output varies by Play services and the selected transport. Cross-platform plugins may generate a second database file whose name never appears in your rules.&lt;/p&gt;

&lt;p&gt;Do not follow this exclusion path if your product must legally retain conversation history across device replacement. Do not use it as a substitute for server-side deletion, because a file that never entered backup can still live in crash dumps or screenshots. Security reviewers, not the on-call junior, should decide whether device-to-device transfer is allowed for any remaining metadata.&lt;/p&gt;

&lt;p&gt;If you run this on one phone, post the device, OS, the exact transition, and whether the transcript recovered, restarted empty, or silently disappeared. Comparable environment evidence is the only thing that makes the next junior’s first PR safer than yours.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Keep Cloud Fallback Out of Radio Wake After Airplane Mode</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:11:32 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-cloud-fallback-out-of-radio-wake-after-airplane-mode-330d</link>
      <guid>https://dev.to/roronoa_/keep-cloud-fallback-out-of-radio-wake-after-airplane-mode-330d</guid>
      <description>&lt;p&gt;You sit with a Pixel on Android 14 or an iPhone on iOS 18, sample chat already installed. The screen is foregrounded, Wi-Fi is associated, and Battery Saver is still off for this first pass. You submit a short prompt, then enable airplane mode before any token reaches the bubble. That single transition tells you whether onboarding shipped an offline contract or only a cafeteria Wi-Fi demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid chat usually fails this first-hour check
&lt;/h2&gt;

&lt;p&gt;Hybrid samples keep an on-device engine for the common prompt and a cloud client for everything else. The cloud client usually lives behind a retry interceptor, a flavor URL, or a latency flag that panics too early. Airplane mode should make that URL unusable, but interceptors still open DNS and can wake cellular when the mode lifts. Your first PR should freeze that client whenever the OS path reports no validated network.&lt;/p&gt;

&lt;p&gt;This is not a model-quality argument and not a placement essay about phones versus regional GPUs. It is a junior onboarding contract: the first hour, the first pull request, and the first rollback of the fallback host. If the typing indicator hangs, then suddenly talks to the network after radios return, the demo you saw on Wi-Fi was not engineering yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Record these facts before you touch product code
&lt;/h3&gt;

&lt;p&gt;Write the environment into the PR description so a reviewer can replay the same transition.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Device marketing name, SoC if you know it, and the exact OS build string&lt;/li&gt;
&lt;li&gt;App stack and versions: native, React Native, or Flutter, plus the HTTP client library&lt;/li&gt;
&lt;li&gt;Starting network: office Wi-Fi, cellular only, or already offline&lt;/li&gt;
&lt;li&gt;Permission state for microphone, local network, and background refresh, even if unused&lt;/li&gt;
&lt;li&gt;Power state: charging, Battery Saver / Low Power Mode, or neither&lt;/li&gt;
&lt;li&gt;The lifecycle step: airplane mode during an in-flight prompt, not after the bubble finishes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not treat an emulator airplane toggle as the same experiment as a physical radio. Emulators often fake connectivity without the DNS, interface, and wakeup behavior you will see on hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventory the checkout in the first hour
&lt;/h2&gt;

&lt;p&gt;You should grep the repository before rewriting prompts, because fallback hostnames hide in more places than &lt;code&gt;.env.example&lt;/code&gt;. Run this from the app root and paste the hits you actually intend to gate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Record matching paths in the PR. Do not commit secrets you uncover.&lt;/span&gt;
rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"https?://|inference|fallback|baseUrl|BASE_URL|generativelanguage"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!**/node_modules/**'&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!**/Pods/**'&lt;/span&gt; &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!**/.git/**'&lt;/span&gt;

rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"OkHttp|NSURLSession|Alamofire|Dio|chopper|ConnectivityManager|NWPathMonitor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--glob&lt;/span&gt; &lt;span class="s1"&gt;'!**/node_modules/**'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write a three-line inventory a teammate can challenge.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On-device engine and the packaged asset or module that actually loads it.&lt;/li&gt;
&lt;li&gt;Cloud fallback class and the hostname it resolves in this flavor.&lt;/li&gt;
&lt;li&gt;Build flag that must prevent constructing that client when the path is unvalidated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot name those three things after the first hour, you are not ready to claim offline support in the pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed airplane-mode experiment
&lt;/h2&gt;

&lt;p&gt;This is a proposed single-device experiment, not a lab result with invented first-token times. Copy the steps, fill in your versions, and report whether the UI recovered, restarted, or silently disappeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preconditions you should not skip
&lt;/h3&gt;

&lt;p&gt;You need a physical device, a flavor that still contains the on-device model, and a build that still compiles the cloud client. Keep Battery Saver off for pass A so power policy is not the hidden variable. Skip VPN profiles for this hour, because leftover tunnels make airplane mode look satisfied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Launch the chat screen and confirm the composer is focused in the foreground session.&lt;/li&gt;
&lt;li&gt;Confirm Battery Saver or Low Power Mode is off so pass A has a clean power baseline.&lt;/li&gt;
&lt;li&gt;Send a prompt the on-device path should handle without tools, browsing, or extra host calls.&lt;/li&gt;
&lt;li&gt;Enable airplane mode within one second so Wi-Fi and cellular drop during the in-flight request.&lt;/li&gt;
&lt;li&gt;Wait thirty seconds without tapping timeouts, unlocking dialogs, or backgrounding the app.&lt;/li&gt;
&lt;li&gt;Note a local answer, a closed offline string, or a spinner that never resolves into UI.&lt;/li&gt;
&lt;li&gt;Repeat pass B with Battery Saver or Low Power Mode already enabled before you submit.&lt;/li&gt;
&lt;li&gt;Repeat pass C by enabling airplane mode first, then sending a cold prompt with radios down.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Contract you can assert without fake benchmarks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pass A answers on-device or fails closed with offline copy, never with a hostname error string.&lt;/li&gt;
&lt;li&gt;Pass A leaves no DNS lookup and no TLS handshake in logcat or Console after the airplane toggle.&lt;/li&gt;
&lt;li&gt;Pass B may stretch token time under Low Power Mode, but it must not flip the flag toward cloud.&lt;/li&gt;
&lt;li&gt;Pass C must not enqueue a retry that fires later when the user disables airplane mode.&lt;/li&gt;
&lt;li&gt;Restoring radios without a resubmit must not replay the prompt against the fallback host.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a retry fires when radios return, the first PR is incomplete even when the Wi-Fi demo looked polished.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android: capture after pass A, before you disable airplane mode.&lt;/span&gt;
adb logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | rg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"okhttp|dns|tls handshake|fallback|Unable to resolve host"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On iOS, filter Console for your bundle identifier during the same window and look for &lt;code&gt;nw_connection&lt;/code&gt; or &lt;code&gt;NSURLSession&lt;/code&gt; work that starts after the toggle. Any new connection there is a failed gate, not a flaky staging anecdote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: refuse cloud work before DNS starts
&lt;/h2&gt;

&lt;p&gt;Keep the gate beside the HTTP client, not inside a prompt formatter that still constructs the call. The operating system already knows whether a validated path exists. Ask it before you allocate OkHttp, &lt;code&gt;URLSession&lt;/code&gt;, or Dio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android gate (proposed Kotlin)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InferencePathGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;connectivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ConnectivityManager&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;allowCloudFallback&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;network&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activeNetwork&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;caps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connectivity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNetworkCapabilities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;network&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hasTransport&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NetworkCapabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TRANSPORT_WIFI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
            &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NetworkCapabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TRANSPORT_CELLULAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
            &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NetworkCapabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TRANSPORT_ETHERNET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hasInternet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasCapability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NetworkCapabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NET_CAPABILITY_INTERNET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasCapability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NetworkCapabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NET_CAPABILITY_VALIDATED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hasTransport&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;hasInternet&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AirplaneSafeInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;InferencePathGate&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Interceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Interceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allowCloudFallback&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cloud fallback blocked: no validated path"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;proceed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;allowCloudFallback()&lt;/code&gt; on the same path you would have opened the client. Throwing before &lt;code&gt;chain.proceed&lt;/code&gt; is the point: you never give the interceptor a chance to resolve a name.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS gate (proposed Swift)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Network&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;InferencePathGate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NWPathMonitor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"inference.path.gate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NWPath&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pathUpdateHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;newPath&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newPath&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;allowCloudFallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;satisfied&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usesInterfaceType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wifi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usesInterfaceType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cellular&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
            &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;usesInterfaceType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wiredEthernet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not treat &lt;code&gt;path.status == .satisfied&lt;/code&gt; as permission to talk to the public internet. Airplane mode with a leftover VPN or local interface can still look satisfied until you check the transport you actually intended.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-platform juniors: native gate, thin bridge
&lt;/h3&gt;

&lt;p&gt;If you joined a React Native or Flutter repo, still implement the gate in native code for this first PR. JavaScript reachability plugins often lag the radio by about a second, which is enough for a retry interceptor to start work you cannot see in the JS console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed only. Do not treat this plugin as the final airplane-mode gate.&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:connectivity_plus/connectivity_plus.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;allowCloudFallbackJsLayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Connectivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;checkConnectivity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConnectivityResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;none&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConnectivityResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;wifi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConnectivityResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mobile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConnectivityResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ethernet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expose one method, &lt;code&gt;allowCloudFallback(): Boolean&lt;/code&gt;, and refuse to import or construct the cloud SDK when it is false. The first PR can be that bridge plus the three airplane-mode passes, without a prompt rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  First PR, then the first rollback of the host
&lt;/h2&gt;

&lt;p&gt;Your pull request should carry the inventory, the three passes, and a rollback a teammate can run after you leave Slack. Rollback is not “delete the on-device model.” Rollback is “stop constructing the fallback client, then destroy the hostname you used while proving the gate.”&lt;/p&gt;

&lt;p&gt;Work the rehearsal in this order so you do not debug against shared staging.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Flip the flavor flag so the fallback client is never constructed in that build.&lt;/li&gt;
&lt;li&gt;Re-run pass C and confirm you still answer locally or fail closed with offline copy.&lt;/li&gt;
&lt;li&gt;Restore the flag and point &lt;code&gt;FALLBACK_BASE_URL&lt;/code&gt; at a throwaway host you control.&lt;/li&gt;
&lt;li&gt;Re-run airplane mode and confirm that host’s access log stays empty, then delete the host.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# Uncommitted local flavor. Do not paste real tokens into the PR.
&lt;/span&gt;&lt;span class="py"&gt;FALLBACK_ENABLED&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;FALLBACK_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://your-throwaway-host.example&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A junior engineer should not prove this gate against the team’s long-lived staging inference box. That hostname shows up in other people’s captures, and it trains you to ignore airplane-mode failures because “staging was flaky today.”&lt;/p&gt;

&lt;p&gt;When you need a personal fallback URL for step 3, a short-lived server is enough. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option you can use as that throwaway host, then discard after the rollback rehearsal. Keep the URL out of the default flavor once the path gate is proven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table to paste under the PR test plan
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Cloud client&lt;/th&gt;
&lt;th&gt;On-device engine&lt;/th&gt;
&lt;th&gt;Radio / DNS&lt;/th&gt;
&lt;th&gt;User-visible result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wi-Fi, charging, foreground&lt;/td&gt;
&lt;td&gt;allowed only if local engine declines&lt;/td&gt;
&lt;td&gt;first choice&lt;/td&gt;
&lt;td&gt;normal&lt;/td&gt;
&lt;td&gt;answer or explicit local fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airplane during in-flight prompt&lt;/td&gt;
&lt;td&gt;must not start&lt;/td&gt;
&lt;td&gt;continue or cancel locally&lt;/td&gt;
&lt;td&gt;no new DNS&lt;/td&gt;
&lt;td&gt;answer, offline copy, or cancelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airplane before a cold prompt&lt;/td&gt;
&lt;td&gt;must not start&lt;/td&gt;
&lt;td&gt;run or fail closed&lt;/td&gt;
&lt;td&gt;no new DNS&lt;/td&gt;
&lt;td&gt;no spinner that later hits cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low Power Mode plus airplane&lt;/td&gt;
&lt;td&gt;must not start&lt;/td&gt;
&lt;td&gt;may be slower&lt;/td&gt;
&lt;td&gt;no radio wake&lt;/td&gt;
&lt;td&gt;same contract, different latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Radios return without resubmit&lt;/td&gt;
&lt;td&gt;must not flush a queue&lt;/td&gt;
&lt;td&gt;idle&lt;/td&gt;
&lt;td&gt;no surprise TLS&lt;/td&gt;
&lt;td&gt;wait for the user&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If any row depends on “the interceptor will 500 anyway,” the gate is not done. Fail closed in the UI instead of waiting for a host error that also proves you woke the radio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This workflow does not measure model quality, tokens per second, or thermal headroom on a given SoC. It also does not replace a privacy review of logcat, bugreports, screenshots, or backup artifacts. Skip it if your app has no on-device path and honestly requires the network for every token. Skip it if you only have an emulator, because you cannot observe radio wake there with confidence. Skip it if a legal or safety flow must reach a server even while offline; that flow needs an explicit online requirement, not a silent fallback.&lt;/p&gt;

&lt;p&gt;Do not copy latency numbers from another phone into your onboarding PR. Different compile caches, thermal states, and NNAPI or Core ML backends will move first-token time without changing the airplane-mode contract you are actually shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for comparable evidence, not vibes
&lt;/h2&gt;

&lt;p&gt;Leave the device name, OS build, and the exact transition you ran: airplane during in-flight, airplane then cold prompt, or Low Power Mode combined. Say whether the UI recovered with a local answer, restarted the request, or silently disappeared when radios returned. If cloud traffic still appeared, name the class that opened the socket so the next junior can grep it in the first hour.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>ai</category>
    </item>
    <item>
      <title>Keep Platform Speech Recognition On-Device on Your First Mobile AI PR</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Wed, 16 Sep 2026 03:31:41 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-platform-speech-recognition-on-device-on-your-first-mobile-ai-pr-65g</link>
      <guid>https://dev.to/roronoa_/keep-platform-speech-recognition-on-device-on-your-first-mobile-ai-pr-65g</guid>
      <description>&lt;p&gt;You join the mobile AI squad on Tuesday with a Pixel 8a running Android 15 and a debug APK already installed. The dictation screen sits in the foreground with RECORD_AUDIO granted and SpeechRecognizer already listening. Nobody set the on-device recognizer extra, and a teammate still calls the platform API a local feature. You revoke microphone access from Settings, return without force-stopping, and watch for a leftover network socket.&lt;/p&gt;

&lt;p&gt;That permission transition is the real first hour on this squad, not a demo after standup. You should not polish a stakeholder transcript before the recognizer is proven to stay local. Prove that speech cannot leave the device after a revoke, and that you can roll the flag back. A prior commit may already have opened the vendor cloud path without anyone noticing in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the platform recognizer as a network API until proven otherwise
&lt;/h2&gt;

&lt;p&gt;Android SpeechRecognizer and iOS SFSpeechRecognizer look on-device because the UI never leaves your activity. Both APIs can ship audio to a vendor cloud unless you opt into the on-device recognizer. Your first PR should make that opt-in explicit, testable, and reversible rather than burying it in a helper. Reviewers should reject the change without guessing which constructor the production binary actually calls.&lt;/p&gt;

&lt;p&gt;A permission revoke does not always cancel work that already started on a background thread. Partial results, pending listeners, and cached PCM can outlive the Settings toggle by several seconds. You need a lifecycle experiment on hardware, not another prompt change in the dictation copy. Treat every "it is local by default" comment in Slack as unverified until a capture says so.&lt;/p&gt;

&lt;h3&gt;
  
  
  What on-device has to mean in the PR description
&lt;/h3&gt;

&lt;p&gt;Write the PR so a reviewer can reject it without guessing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The production recognizer is created with the on-device constructor or the on-device request flag.&lt;/li&gt;
&lt;li&gt;If the device has no on-device model, the UI fails closed and never opens a network recognizer.&lt;/li&gt;
&lt;li&gt;A microphone revoke stops listening, drops in-memory PCM, and does not start cloud work.&lt;/li&gt;
&lt;li&gt;A feature flag can restore a disabled state in one release if the on-device pack is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those four bullets are the golden behavior for this merge. Anything else is a story about accuracy in a quiet room.&lt;/p&gt;

&lt;h2&gt;
  
  
  A proposed single-device experiment, not a benchmark
&lt;/h2&gt;

&lt;p&gt;Do not publish latency numbers from this walkthrough because it is only a pass or fail check. Record the device, OS build, application state, and the exact Settings transition beside every note. Label the whole protocol as proposed until you have those fields filled on a physical phone. A missing speech pack is a valid outcome, and you should write it down before blaming the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Suggested environment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Device: one physical phone you can revoke permissions on, not an emulator image.&lt;/li&gt;
&lt;li&gt;OS: Android 12+ so &lt;code&gt;createOnDeviceSpeechRecognizer()&lt;/code&gt; exists, or iOS 17+ with on-device speech.&lt;/li&gt;
&lt;li&gt;App state: foreground dictation activity, process warm, screen on, charger connected.&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi attached so a mistaken cloud path would have a route.&lt;/li&gt;
&lt;li&gt;Permission: RECORD_AUDIO or Microphone granted at start, then revoked from system Settings.&lt;/li&gt;
&lt;li&gt;Power: stay off battery saver so the OS does not silently throttle listeners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Emulators often fake speech services and will lie about on-device availability. Skip them for this PR, even when the Dart or Kotlin unit tests already pass on CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exact steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install a debug build whose application ID matches the one you will inspect with &lt;code&gt;adb&lt;/code&gt; or Console.&lt;/li&gt;
&lt;li&gt;Confirm the on-device speech pack is installed, or write down that it is missing before you start.&lt;/li&gt;
&lt;li&gt;Open the dictation screen and start listening with a phrase you chose in advance.&lt;/li&gt;
&lt;li&gt;Leave the process running, switch to Settings, and revoke microphone access for the app.&lt;/li&gt;
&lt;li&gt;Return to the dictation screen without force-stopping, and wait thirty seconds.&lt;/li&gt;
&lt;li&gt;Capture logs, a proxy session, and the UI state, then decide whether audio recovered, restarted, or vanished.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Expected observations if the PR is honest
&lt;/h3&gt;

&lt;p&gt;Write the expected result before you touch Settings, then compare it with the capture. After revoke, listening should stop and the UI should show a permission error, not a spinner. The proxy should record no new TLS client hello to speech endpoints during those thirty seconds. Log lines should not print the audio sample, the partial transcript, or a cloud hostname.&lt;/p&gt;

&lt;p&gt;If the buffer silently disappears with no error, that is still a defect for reviewers. The reviewer cannot tell a clean cancel from a swallowed upload without an explicit permission error. Killing and restarting the app must not replay the pre-revoke buffer either. Silent success is how leaked audio hides in a first-week merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard the Android constructor before you touch UI copy
&lt;/h2&gt;

&lt;p&gt;The platform gives you two constructors, and the older factory is the network-friendly default path. Your first PR should call the on-device factory and refuse to fall back to the legacy one. Fail closed when the speech pack is missing, even if that makes the dictation button show an error. Do not hide a cloud recognizer behind a catch block that "keeps the feature working" on older phones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed production path. Confirm on a physical device before you merge.&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;buildRecognizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;SpeechRecognizer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nc"&gt;SpeechRecognizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isOnDeviceRecognitionAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"on-device speech pack missing; fail closed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SpeechRecognizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOnDeviceSpeechRecognizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;buildIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RecognizerIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ACTION_RECOGNIZE_SPEECH&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RecognizerIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_LANGUAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLanguageTag&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RecognizerIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_PREFER_OFFLINE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;putExtra&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RecognizerIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EXTRA_PARTIAL_RESULTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onPermissionRevoked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recognizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SpeechRecognizer&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;recognizer&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;recognizer&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire the revoke path to onStop and to a permission check when the activity resumes from Settings. Do not wait for a future onError from the recognizer, because that callback can arrive late. A late onError often means a network retry already started on a thread you do not control. Clear the PCM buffer in the same function that destroys the recognizer so logs cannot dump it later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed checks for your first PR, run on the local debug APK.&lt;/span&gt;
unzip &lt;span class="nt"&gt;-p&lt;/span&gt; app-debug.apk classes&lt;span class="k"&gt;*&lt;/span&gt;.dex | strings | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; createSpeechRecognizer
unzip &lt;span class="nt"&gt;-p&lt;/span&gt; app-debug.apk classes&lt;span class="k"&gt;*&lt;/span&gt;.dex | strings | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; createOnDeviceSpeechRecognizer
adb shell dumpsys package com.example.dictation | &lt;span class="nb"&gt;grep &lt;/span&gt;RECORD_AUDIO
adb shell appops get com.example.dictation RECORD_AUDIO
adb logcat &lt;span class="nt"&gt;-s&lt;/span&gt; SpeechRecognizer:&lt;span class="k"&gt;*&lt;/span&gt; ActivityManager:&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;tee &lt;/span&gt;first-hour-speech.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want the default factory count at zero in release dex, and the on-device factory present. App ops should read deny after the Settings revoke, and the UI should match that denied state. If strings still contain a speech hostname, treat the PR as failed even when the UI looks correct. Run the same grep on the AAB you plan to ship, not only on the local debug APK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guard the iOS request flag the same morning
&lt;/h2&gt;

&lt;p&gt;SFSpeechRecognizer can still hop to Apple servers when requiresOnDeviceRecognition stays false on the request. Set the flag first, then fail closed if the locale cannot run recognition on the device. Do not keep a second SFSpeechRecognitionRequest around for "when the user is on Wi-Fi." A Wi-Fi-only fallback is still a cloud path, and it will surprise you during the revoke test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed production path. Confirm on a physical iPhone before you merge.&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;SFSpeechAudioBufferRecognitionRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;recognizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SFSpeechRecognizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;recognizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recognizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supportsOnDeviceRecognition&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;SpeechGuardError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onDeviceModelMissing&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SFSpeechAudioBufferRecognitionRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requiresOnDeviceRecognition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shouldReportPartialResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taskHint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dictation&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;dropSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AVAudioEngine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AVAudioPCMBuffer&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputNode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeTap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onBus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;floatChannelData&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pointee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;repeating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;frameLength&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Proposed Console filter while you revoke Microphone in Settings.&lt;/span&gt;
log stream &lt;span class="nt"&gt;--predicate&lt;/span&gt; &lt;span class="s1"&gt;'subsystem CONTAINS "Speech"'&lt;/span&gt; &lt;span class="nt"&gt;--level&lt;/span&gt; debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Juniors often dump debug WAV files into Documents during the first hour of pairing on a loaner phone. That directory can leave the device through a routine iCloud backup you will not see in Charles. Mark every debug recording excluded from backup, or write it only to a cache you delete on revoke. Never commit those WAV files to the repository, even when they contain only your own voice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;URLResourceKey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;isExcludedFromBackupKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nv"&gt;ofItemAtPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;debugWav&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Keep eval phrases off the user's microphone
&lt;/h2&gt;

&lt;p&gt;You still need a tiny golden set so reviewers can exercise the recognizer without recording coworkers. Draft twenty short phrases in the target locale, then speak them on the device at a known volume. Do not upload those recordings to a shared Drive folder, and do not commit WAV files into git. Keep the phrase list in the PR description as text, not as binary fixtures on a public fork.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. If you want a second pair of eyes while drafting that phrase list, MonkeyCode's free model access can help. Use the free server option on the eval laptop so the host never lives inside the mobile binary. Keep that hostname out of APK strings, out of plist files, and out of any runtime fallback URL. The phone's only job is on-device recognition plus a fail-closed error when the pack is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollback if a prior commit already opened the network path
&lt;/h2&gt;

&lt;p&gt;Rehearse rollback in the same PR, because on-device packs are missing on some retail SKUs. Ship a remote flag that chooses the constructor, defaulting to on-device, with a one-release kill switch. If you already shipped a network recognizer, do not fix forward by adding a second constructor. Cut a patch that forces disabled, wait until proxy traffic goes quiet, then ship the on-device flag.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag value&lt;/th&gt;
&lt;th&gt;Constructor&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;th&gt;Risk if stuck&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on_device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createOnDeviceSpeechRecognizer&lt;/code&gt; / &lt;code&gt;requiresOnDeviceRecognition = true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Default after this PR&lt;/td&gt;
&lt;td&gt;Some devices show "model missing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No recognizer created&lt;/td&gt;
&lt;td&gt;Emergency rollback&lt;/td&gt;
&lt;td&gt;Feature disappears, no audio leaves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;legacy_network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Do not ship this value&lt;/td&gt;
&lt;td&gt;Not for production&lt;/td&gt;
&lt;td&gt;Audio can leave the device&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you already shipped &lt;code&gt;legacy_network&lt;/code&gt;, record the store version that still contained the network constructor. Support needs that version number so they can tell users to update before the next privacy review. Do not leave both constructors in the binary behind an undocumented debug extra on the intent. Undocumented extras are how the default factory sneaks back in during a "temporary" support build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who should not merge this as-is
&lt;/h2&gt;

&lt;p&gt;This walkthrough does not claim packet captures, battery figures, or word-error rates from a device class. On-device packs vary by locale, OEM image, and whether the user downloaded a speech language model. Zeroing a PCM buffer does not erase copies already sitting in logd, a vendor HAL, or accessibility. Treat accessibility services and OEM voice assistants as separate trust boundaries you cannot control from app code.&lt;/p&gt;

&lt;p&gt;Do not use this approach if you legally operate cloud STT with a consent screen and a DPA. Do not use it if you only have emulators, because they will not load the same speech packs. Flutter and React Native plugins often call the default Android constructor even when Dart says offline. Read the plugin source for &lt;code&gt;createSpeechRecognizer&lt;/code&gt; before you trust a boolean named &lt;code&gt;onDevice&lt;/code&gt; in Dart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell the channel what you actually saw
&lt;/h2&gt;

&lt;p&gt;Reply with the device model, OS build, and whether the on-device speech pack was actually installed. Include the exact Settings transition, and say whether you returned to the app without force-stopping. Say whether listening recovered, restarted after an error, or silently disappeared from the dictation UI. That field report is more useful to the next junior than another accuracy screenshot from a quiet office.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Your First On-Device AI PR: Ship a Golden Set and Rehearse the Rollback</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Tue, 15 Sep 2026 03:08:49 +0000</pubDate>
      <link>https://dev.to/roronoa_/your-first-on-device-ai-pr-ship-a-golden-set-and-rehearse-the-rollback-3eab</link>
      <guid>https://dev.to/roronoa_/your-first-on-device-ai-pr-ship-a-golden-set-and-rehearse-the-rollback-3eab</guid>
      <description>&lt;p&gt;It is 9:40 on a Tuesday, and a junior engineer on your team has just cloned the notes app repository onto a laptop with 8 GB of RAM and no GPU. The ticket says: add a summary to the note detail screen, and make it work on the phone rather than round-tripping every keystroke to a server. She writes a first version, backgrounds the app mid-inference to answer a message, and comes back to a spinner that never resolves. That single lifecycle transition — backgrounding during inference — is what her first pull request has to survive, and it is the thing most onboarding checklists leave out.&lt;/p&gt;

&lt;p&gt;This article is a proposed test plan, not a measured benchmark. I have not run the numbers below on your hardware, and the code samples are illustrative rather than copy-paste production code. What you get instead is a repeatable structure: one golden set, one decision table, and one rollback rehearsal you can finish in your first hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record the environment before you write product code
&lt;/h2&gt;

&lt;p&gt;You cannot debug an inference path if you do not know what you ran it on. Open a scratch file and fill in every row below before the first commit, then paste the same table into the PR description.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Fill in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;model and, if you have one, the exact variant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;version and build number, not just "Android" or "iOS"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App&lt;/td&gt;
&lt;td&gt;version, git SHA, and build type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;runtime version plus every AI-related dependency pin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;microphone, network, storage — granted or revoked at test time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Wi-Fi, cellular, throttled, or airplane mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;battery percentage and whether the device was charging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transition&lt;/td&gt;
&lt;td&gt;the one lifecycle event the ticket is really about&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice the last row. Pick exactly one transition for your first PR. Backgrounding during inference is a good default because it is cheap to reproduce and it exposes timeout handling immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three gates your first PR has to pass
&lt;/h2&gt;

&lt;p&gt;A junior engineer's first inference PR is not judged on model quality. It is judged on whether a reviewer can answer three questions without opening the app.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;With network present, does the request return a valid, schema-checked result?&lt;/li&gt;
&lt;li&gt;With network absent, permission revoked, or the endpoint returning 5xx, does the feature fall back to something deterministic instead of a spinner?&lt;/li&gt;
&lt;li&gt;Can the feature be turned off from a configuration endpoint without shipping a new build?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can only prove one of the three, prove the third. A kill switch is what lets the rest of the team merge your work without betting the release on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a free endpoint fits, and what you should not assume
&lt;/h2&gt;

&lt;p&gt;Gates 1 and 3 both need a small endpoint you control: something to run the golden set against, and something to serve the feature flag. MonkeyCode is an open-source project whose operator states that it offers free model access, currently described as a 10M-token free allowance, together with a free server option. Treat both as vendor claims: sign in, read the current terms, and confirm the limits in your own account before you build a habit on top of them. Token allowances, available models, and server terms can change, and this article does not assert any specific model name, hardware spec, or performance number.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;Use the free model access for the golden-set runner and the free server for the flag endpoint and, if you like, a scheduled re-run of the same runner. Do not use it as the only path your app can take — see the rollback section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a 12-case golden set before you tune a prompt
&lt;/h2&gt;

&lt;p&gt;Twelve cases is enough to catch schema drift and empty-output bugs, and small enough that a junior engineer can read every line in review. Store it as JSONL so the runner and any future harness can consume the same file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":"short-note","input":"Groceries: milk, eggs, bread.","max_chars":120,"must_be_single_paragraph":true}
{"id":"unicode-emoji","input":"Ideas 🎧 for the commute, plus a reminder to buy 🥛.","max_chars":120,"must_be_single_paragraph":true}
{"id":"empty-note","input":"","max_chars":120,"expect_fallback":true}
{"id":"very-long","input":"&amp;lt;paste 3000 characters of real notes from your own device&amp;gt;","max_chars":120,"must_be_single_paragraph":true}
{"id":"permission-revoked","input":"Summarize this with the microphone permission denied.","expect_fallback":true}
{"id":"offline","input":"Summarize this with radio off.","expect_fallback":true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important rows are the boring ones: empty input, revoked permission, and offline. Those are the states where a demo-grade implementation quietly returns &lt;code&gt;null&lt;/code&gt; and the UI decides to show a spinner forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the golden set from a small server
&lt;/h2&gt;

&lt;p&gt;The runner below is an unexecuted example. Set the base URL and key from your own project dashboard rather than hard-coding anything, and keep the API key in the server environment, never in the app bundle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MONKEYCODE_BASE_URL&lt;/span&gt;:?set&lt;span class="p"&gt; from your project dashboard&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
: &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MONKEYCODE_API_KEY&lt;/span&gt;:?set&lt;span class="p"&gt; this in the server environment&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL&lt;/span&gt;:?pick&lt;span class="p"&gt; a model from your dashboard&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;OUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"golden-report.tsv"&lt;/span&gt;

&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'id\tstatus\thttp\tttfb_ms\tschema_ok\tchars\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; line&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; .id &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; .input &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s%3N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/body.json &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MONKEYCODE_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/chat/completions"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$MONKEYCODE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; m &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; p &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="s1"&gt;'{model:$m,messages:[{role:"user",content:$p}],max_tokens:160,temperature:0}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;ttfb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s%3N&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; start &lt;span class="k"&gt;))&lt;/span&gt;

  &lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.choices[0].message.content // empty'&lt;/span&gt; /tmp/body.json&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;chars&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;text&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;schema_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;schema_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\t%s\t%s\t%s\t%s\t%s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$http&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 200 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;ok &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;fail&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$http&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ttfb&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$schema_ok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$chars&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; golden_set.jsonl

column &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it once, paste the table into the PR, and commit the script next to the app code so the next new hire inherits it. Two rules matter more than the code: keep &lt;code&gt;temperature&lt;/code&gt; at 0 so diffs are readable, and record the wall-clock time of the run so a reviewer knows which endpoint revision produced it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pad the decision table with your own constraints
&lt;/h2&gt;

&lt;p&gt;Before you defend a placement decision in review, write down what you actually need. The table below is a structure, not a recommendation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Latency you can promise&lt;/th&gt;
&lt;th&gt;Offline behavior&lt;/th&gt;
&lt;th&gt;Data egress&lt;/th&gt;
&lt;th&gt;Rollback cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fully on-device&lt;/td&gt;
&lt;td&gt;depends on your model size and device class&lt;/td&gt;
&lt;td&gt;works&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;app release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted endpoint&lt;/td&gt;
&lt;td&gt;network round trip plus queueing&lt;/td&gt;
&lt;td&gt;fallback needed&lt;/td&gt;
&lt;td&gt;note text leaves the device&lt;/td&gt;
&lt;td&gt;flag flip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid, flag-gated&lt;/td&gt;
&lt;td&gt;hosted when healthy, local otherwise&lt;/td&gt;
&lt;td&gt;degraded but deterministic&lt;/td&gt;
&lt;td&gt;conditional&lt;/td&gt;
&lt;td&gt;flag flip plus cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fill the latency column with measurements from your own device, not with numbers copied from someone else's blog post. If you cannot measure it, say so in the PR instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rehearse the rollback in the first hour, not the first incident
&lt;/h2&gt;

&lt;p&gt;A kill switch you have never exercised is a comment, not a control. Rehearse it once, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable the flag in your config endpoint and confirm the remote path is used.&lt;/li&gt;
&lt;li&gt;Disable the flag, force-stop the app, and relaunch. The local fallback should appear with no crash and no empty state.&lt;/li&gt;
&lt;li&gt;Disable the flag while the app is in the foreground and trigger a summary. Your code must read the flag per call, not once at startup.&lt;/li&gt;
&lt;li&gt;Point the app at an unreachable endpoint and repeat step 3 to prove the timeout path, not just the flag path.&lt;/li&gt;
&lt;li&gt;Enable airplane mode, then revoke the microphone permission, and repeat step 3 for each state.&lt;/li&gt;
&lt;li&gt;Restore everything, and confirm the remote path returns without a reinstall.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cache the last known flag value locally. If the config endpoint itself is down, an app that depends on a live fetch to decide whether to crash or fall back has simply moved the outage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative, not compiled. Gate the remote call on the cached flag&lt;/span&gt;
&lt;span class="c1"&gt;// and on a network check, and always keep the local path available.&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SummarizeNoteUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FeatureFlags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// cached, refreshed in background&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RemoteSummarizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ExtractiveSummarizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Summary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;noteSummaryEnabled&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isReachable&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrElse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set your request timeout below the point where a user typically leaves the screen, and measure that interval on your own device rather than assuming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in the PR description
&lt;/h2&gt;

&lt;p&gt;Keep it short and evidence-shaped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The environment table from the top of this article.&lt;/li&gt;
&lt;li&gt;The golden-set report, including the offline and permission-revoked rows.&lt;/li&gt;
&lt;li&gt;Which lifecycle transition you tested, and what the app did after it.&lt;/li&gt;
&lt;li&gt;The rollback steps you ran, and whether the feature recovered, restarted, or silently disappeared.&lt;/li&gt;
&lt;li&gt;What you did not test, stated plainly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations and who should not copy this
&lt;/h2&gt;

&lt;p&gt;This workflow is a starting point, and it has edges worth naming out loud. It will not tell you anything about model quality on long or adversarial inputs, because twelve cases cannot. It produces no battery or thermal numbers unless you add a measurement step and a consistent power state. It assumes you have some form of remote configuration; if your app has none, build the local fallback first and treat the flag as a later addition. And if your notes contain regulated data, a hosted endpoint is a compliance decision that a golden set cannot make for you.&lt;/p&gt;

&lt;p&gt;Skip the hosted path entirely if your product needs deterministic sub-second behavior with no network dependency, if you cannot measure latency on a real device, or if you have no way to serve a flag. A local-only extractive summary is a perfectly respectable first PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send me the environment, not the opinion
&lt;/h2&gt;

&lt;p&gt;If you try this on your own repo, I am more interested in your setup than in your verdict. Post the device, OS version, the transition you tested, and whether the feature recovered, restarted, or silently disappeared after it. Those four fields are what turn an onboarding story into something the next junior engineer can reproduce.&lt;/p&gt;

&lt;p&gt;If you need a model endpoint and a small server to run the golden set while you onboard, MonkeyCode's free model access and free server option is one place to start — check the current terms in your own account first, and keep the fallback path working regardless of what the free tier looks like next month.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ai</category>
      <category>mobile</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Keep First-Hour Inference Hosts Out of Android Bugreports</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sun, 13 Sep 2026 19:19:45 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-first-hour-inference-hosts-out-of-android-bugreports-25e3</link>
      <guid>https://dev.to/roronoa_/keep-first-hour-inference-hosts-out-of-android-bugreports-25e3</guid>
      <description>&lt;p&gt;You sit with a Pixel 8 running Android 15, a React Native debug build, and a live voice eval still on screen. The first completion hangs, so you press Home, background the activity, and wait for the socket to die. A teammate asks for a bugreport zip, and you capture one because that feels like a normal first-hour debug move. That archive now holds the staging inference host you pointed at during onboarding, including paths you never meant to share.&lt;/p&gt;

&lt;p&gt;Junior engineers hit this during the first hour more often than architecture reviews admit. You need a remote completion path because the on-device package is still gated behind another pull request. A debug host feels disposable until Android writes it into logcat, connectivity dumps, and crash breadcrumbs. The rest of this article is a proposed leak test, not a published benchmark with invented timings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a first-hour host shows up in the zip
&lt;/h2&gt;

&lt;p&gt;A debug inference URL is not only a Kotlin constant sitting in &lt;code&gt;BuildConfig&lt;/code&gt;. OkHttp, Flipper, React Native networking, and your retry wrapper all echo that host when a stream stalls. Backgrounding a hung voice call is enough for the system to snapshot sockets, and &lt;code&gt;adb bugreport&lt;/code&gt; keeps those lines. You also persist values that were supposed to live for a single morning.&lt;/p&gt;

&lt;p&gt;SharedPreferences, MMKV, and AsyncStorage survive the session you treated as ephemeral. The first rollback then deletes the constant while the cached host remains on disk. Reviewers who only read the diff will miss that leftover. Treat the zip as part of the PR evidence, not as a side channel you share casually.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you should treat as sensitive
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The staging inference base URL and any path prefix that names the model route&lt;/li&gt;
&lt;li&gt;Temporary session ids that bind a microphone buffer to that host&lt;/li&gt;
&lt;li&gt;Debug bearer tokens pasted for hour one (never write these to logcat)&lt;/li&gt;
&lt;li&gt;The product flavor that still compiles the remote fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not treat the host as harmless because it is not production. Bugreports leave the building when you file a ticket or drop a zip into chat. Play pre-launch reports and teammate laptops are also outside your debug device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed lab, not a measured claim
&lt;/h2&gt;

&lt;p&gt;Record the environment before you change code. The setup below is a single-device experiment you can reproduce, not a claim that these exact builds were already timed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Device:&lt;/strong&gt; Pixel 8, or another Android 14+ phone you physically hold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS:&lt;/strong&gt; Android 15, developer options on, USB debugging authorized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App:&lt;/strong&gt; React Native debug APK, microphone granted, unmetered Wi-Fi&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power:&lt;/strong&gt; plugged in, battery optimization left at the package default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; voice eval foregrounded, then Home to background the activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition:&lt;/strong&gt; hung completion → background → &lt;code&gt;adb bugreport&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected observation: the staging host appears under logcat or dumpsys inside &lt;code&gt;bugreport-*.zip&lt;/code&gt;. The recovery outcome you want is stricter. After the flavor gate and the first rollback, a clean install plus a new bugreport must not contain that host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate the host in a debug source set
&lt;/h2&gt;

&lt;p&gt;Keep the remote fallback out of the release tree. A proposed Kotlin split looks like the following, and you should treat it as scaffolding rather than production networking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// android/app/src/debug/java/com/example/inference/DebugInferenceHosts.kt&lt;/span&gt;
&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;DebugInferenceHosts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;STAGING_BASE_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://staging.example.invalid/v1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// android/app/src/release/java/com/example/inference/DebugInferenceHosts.kt&lt;/span&gt;
&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;DebugInferenceHosts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;STAGING_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;inferenceBaseUrl&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;debugHost&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DebugInferenceHosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STAGING_BASE_URL&lt;/span&gt;
    &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DEBUG&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;debugHost&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"Remote fallback is debug-only; use the on-device path."&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;debugHost&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Release compilation should fail closed if someone calls the remote path from shared code. You want a CI crash, not a silent production fallback that looks like an agent. React Native still needs the same fail-closed read on the JavaScript side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/inference/host.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NativeModules&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDebugInferenceHost&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;__DEV__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NativeModules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DebugInferenceHosts&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stagingBaseUrl&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;__DEV__&lt;/code&gt; is false, do not read AsyncStorage for a leftover URL. Juniors often restore a session on first launch and rehydrate the debug host into a release candidate. That rehydrate path is how a rollback looks complete in git and still fails on device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop writing the host into logcat
&lt;/h2&gt;

&lt;p&gt;OkHttp logging is the usual gift inside a bugreport. Keep verbose loggers out of the main source set, and install them only from debug code. &lt;code&gt;BASIC&lt;/code&gt; still prints the host, which is fine on a private phone and not fine in a zip you attach to a ticket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpLoggingInterceptor&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpLoggingInterceptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BASIC&lt;/span&gt;
                &lt;span class="nf"&gt;redactHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a hung voice call, log a local correlation id only. Never interpolate &lt;code&gt;request.url&lt;/code&gt;. Never put the URL in Crashlytics keys, breadcrumb strings, or analytics events that survive process death.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;w&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"VoiceEval"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"completion stalled; localSession=$localSessionId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your interceptor pretty-prints JSON bodies, disable it before you reproduce the hang. Body loggers will also retain prompt fragments, which is a separate leak from the host itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducible bugreport grep
&lt;/h2&gt;

&lt;p&gt;After the background hang, search the archive before you file anything. The commands below are the artifact; run them on the same device you used for the voice eval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb bugreport ./bugreport-first-hour.zip
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/br &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unzip &lt;span class="nt"&gt;-q&lt;/span&gt; ./bugreport-first-hour.zip &lt;span class="nt"&gt;-d&lt;/span&gt; /tmp/br

&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"staging.example.invalid"&lt;/span&gt;
rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /tmp/br &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"host not found in bugreport"&lt;/span&gt;

&lt;span class="c"&gt;# Fail the first PR if release dex still contains the host&lt;/span&gt;
unzip &lt;span class="nt"&gt;-p&lt;/span&gt; app/build/outputs/apk/release/app-release.apk classes&lt;span class="k"&gt;*&lt;/span&gt;.dex &lt;span class="se"&gt;\&lt;/span&gt;
  | strings &lt;span class="se"&gt;\&lt;/span&gt;
  | rg &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: host survived release dex"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"release dex is clean"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull preferences as well, because a clean dex does not mean a clean device.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell run-as com.example.app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;cat &lt;/span&gt;shared_prefs/inference.xml &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;adb shell run-as com.example.app &lt;span class="nb"&gt;ls &lt;/span&gt;files &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the XML still holds the host after you removed the constant, the first rollback is incomplete. Repeat the grep after uninstall and reinstall, not only after &lt;code&gt;assembleRelease&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle checks before you open the PR
&lt;/h2&gt;

&lt;p&gt;Run these in order on one device. Record pass or fail, not impressions. Keep microphone state, network state, and power state in the PR body beside the diff.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Permission revoke.&lt;/strong&gt; Open Settings → Apps → your app → Microphone → Don't allow, then resume the eval. The client should stop capture and must not dump the last request URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backgrounding.&lt;/strong&gt; With a live stream, press Home and wait thirty seconds. Resume and confirm you only logged &lt;code&gt;localSessionId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process death.&lt;/strong&gt; Run &lt;code&gt;adb shell am kill com.example.app&lt;/code&gt;, relaunch, and confirm storage did not restore the debug host on a release-shaped build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First rollback.&lt;/strong&gt; Delete the debug host from the debug source set, assemble release, then grep dex and a fresh bugreport.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Decision table for hour one
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Remote host allowed?&lt;/th&gt;
&lt;th&gt;What you still grep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debug flavor, mic granted, first hour&lt;/td&gt;
&lt;td&gt;Staging only&lt;/td&gt;
&lt;td&gt;bugreport zip, logcat, prefs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug flavor, mic revoked&lt;/td&gt;
&lt;td&gt;None; stop capture&lt;/td&gt;
&lt;td&gt;last request logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release candidate&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;APK strings, crash keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After rollback tag&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;device prefs, leftover MMKV files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is a checklist, not a claim that every OEM dump looks identical. If a line fails, block the PR even when the feature demo still talks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a throwaway host belongs
&lt;/h2&gt;

&lt;p&gt;Hour one still needs somewhere to send a test completion when the on-device model is not packaged. Point the debug flavor at a host you control, not at production inference, and not at a laptop that will sleep during review. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option you can use as that disposable staging target while you prove the flavor gate, without borrowing production credentials. It does not replace on-device packaging, permission handling, or the grep steps, and this article does not claim model names, quotas, hardware, or uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This approach is debug-flavor scaffolding for a junior joining a mobile AI repo. It is the wrong path for customer audio, paid production inference, or any SKU that must stay on-device. Healthcare, offline-only catalogs, and anything covered by a signed model license should not use a free staging box as a fallback.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;strings&lt;/code&gt; on dex can miss split URLs and obfuscated concatenation, so a clean grep is necessary rather than sufficient. iOS sysdiagnose is a different artifact, and you should not assume the Android zip search transfers. This article also skips energy numbers, because a leak test is not a battery benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  First rollback, then open the PR
&lt;/h2&gt;

&lt;p&gt;Your first PR should include the flavor files, the fail-closed React Native read, the log redaction, and a note of the bugreport grep. The description should record device, OS, permission state, network state, and whether the hung session recovered, restarted, or silently disappeared. After merge, run the rollback on a clean install once. If the host returns from disk, onboarding is not done; only the diff is.&lt;/p&gt;

&lt;p&gt;If you run the same transition, reply with device, OS, the background and revoke steps, and whether the host recovered, restarted, or vanished from the next bugreport.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>privacy</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>Keep Model Shards Out of Shared Storage on Your First Mobile AI PR</title>
      <dc:creator>Roronoa</dc:creator>
      <pubDate>Sat, 12 Sep 2026 17:29:46 +0000</pubDate>
      <link>https://dev.to/roronoa_/keep-model-shards-out-of-shared-storage-on-your-first-mobile-ai-pr-4pnh</link>
      <guid>https://dev.to/roronoa_/keep-model-shards-out-of-shared-storage-on-your-first-mobile-ai-pr-4pnh</guid>
      <description>&lt;p&gt;You clone the team's React Native repository onto a Pixel 8 running Android 15 with a half-charged battery. The README tells you to launch the sample, grant storage permission, and wait while an on-device model downloads. You background the app during that transfer, then reopen Files after a process death, and an incomplete shard sits in Downloads. That first-hour leak is the problem this article walks you through, before your first PR and before your first rollback.&lt;/p&gt;

&lt;p&gt;This write-up is a proposed onboarding experiment, not a farm-wide benchmark and not a source of invented timings. You should record the device, OS, framework version, network, and permission state before you treat any observation as evidence. The goal is that model bytes stay in app-private storage across backgrounding, process death, and a later config rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why shared storage shows up in your first hour
&lt;/h2&gt;

&lt;p&gt;Junior engineers often copy a DownloadManager snippet that targets the public Downloads collection because it appears to work. Android then indexes those files through MediaStore, and Files or a USB cable can list every incomplete shard. iOS has a quieter version of the same mistake when you write into a shared container or skip the backup exclusion flag.&lt;/p&gt;

&lt;p&gt;On-device inference makes the blast radius worse than a misplaced JSON configuration sitting in a cache directory. Model shards are large, sometimes user-specific after packaging, and they survive the activity that downloaded them. A first rollback of the model URL does not delete public files the operating system already indexed. Your first hour on the repo is exactly when those leftovers show up in a demo that still looks fine.&lt;/p&gt;

&lt;p&gt;Watch for these first-hour smells in the sample you just cloned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sample requests &lt;code&gt;READ_MEDIA_IMAGES&lt;/code&gt; or another broad storage grant only to fetch a model.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DownloadManager.Request&lt;/code&gt; calls &lt;code&gt;setDestinationInExternalPublicDir&lt;/code&gt; toward public Downloads.&lt;/li&gt;
&lt;li&gt;React Native &lt;code&gt;react-native-fs&lt;/code&gt; writes into &lt;code&gt;DownloadDirectoryPath&lt;/code&gt; or &lt;code&gt;ExternalStorageDirectoryPath&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;iOS code copies a downloaded weight into a document the user can browse inside Files.&lt;/li&gt;
&lt;li&gt;A retry queue keeps &lt;code&gt;.gguf.part&lt;/code&gt; or &lt;code&gt;.tflite.tmp&lt;/code&gt; sitting beside the finished file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coding assistants are everywhere in onboarding chats this year, which makes the leak easier to spread. A teammate pastes the downloader, the assistant repeats the public-path snippet, and your first PR ships the same Files.app surprise. Treat the assistant as a reviewer of a private destination helper, not as a source of storage permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proposed first-hour experiment
&lt;/h2&gt;

&lt;p&gt;Label this as a procedure you run on one device, then compare notes with a teammate who owns a different OS build. Do not treat the steps as proof that every OEM Files app hides app-specific external directories the same way. Fill the environment block with the hardware on your desk before you file a bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example environment to record
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Device: Pixel 8 or iPhone 15, with the actual board name if you have it.&lt;/li&gt;
&lt;li&gt;OS: Android 15 or iOS 18, plus the security patch string on Android.&lt;/li&gt;
&lt;li&gt;App: React Native 0.76, Flutter 3.24, or whatever your lockfile pins today.&lt;/li&gt;
&lt;li&gt;Network: Wi-Fi first, then a mid-transfer switch onto cellular.&lt;/li&gt;
&lt;li&gt;Power: battery saver off, then on, without inventing drain percentages.&lt;/li&gt;
&lt;li&gt;Permissions: storage or photos granted at launch, then revoked from Settings.&lt;/li&gt;
&lt;li&gt;Application state: foreground download, Home, process death, then a cold relaunch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exact steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install a debug build from a clean profile so leftover models are not already present on disk.&lt;/li&gt;
&lt;li&gt;Start the model download, then press Home while the progress affordance is still active.&lt;/li&gt;
&lt;li&gt;Force-stop the app from Settings so you simulate process death in the middle of the transfer.&lt;/li&gt;
&lt;li&gt;Open the system Files app and search for the model name, &lt;code&gt;.part&lt;/code&gt;, &lt;code&gt;.tmp&lt;/code&gt;, &lt;code&gt;.gguf&lt;/code&gt;, and &lt;code&gt;.tflite&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Connect &lt;code&gt;adb&lt;/code&gt; or Finder and list public Downloads beside the app-specific models directory.&lt;/li&gt;
&lt;li&gt;Restore the previous model URL as your first config rollback, then relaunch the app.&lt;/li&gt;
&lt;li&gt;Search Files again and write down whether the incomplete shard recovered, restarted, or silently remained.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Expected observations, not measured claims
&lt;/h3&gt;

&lt;p&gt;Public Downloads should contain zero model shards after a correct implementation lands in your first PR. App-private storage may still contain a truncated file, and your loader must refuse that file until a checksum matches. Rollback should not resurrect a public MediaStore row for the old URL after the force-stop. A permission revoke should stop a new public write rather than strand a world-readable temp file beside the old shard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commands you can run during that hour
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android: public Downloads versus app-private models&lt;/span&gt;
adb shell &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /sdcard/Download | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'gguf|tflite|onnx|part|tmp'&lt;/span&gt;
adb shell run-as com.example.app &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; files/models

&lt;span class="c"&gt;# Android: MediaStore hits for model-like names&lt;/span&gt;
adb shell content query &lt;span class="nt"&gt;--uri&lt;/span&gt; content://media/external/file &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--projection&lt;/span&gt; _display_name:_data:size &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--where&lt;/span&gt; &lt;span class="s2"&gt;"_display_name LIKE '%.gguf%' OR _display_name LIKE '%.tflite%'"&lt;/span&gt;

&lt;span class="c"&gt;# iOS Simulator: Application Support versus user-visible Documents&lt;/span&gt;
xcrun simctl get_app_container booted com.example.app data
&lt;span class="c"&gt;# then inspect Library/Application Support/models and Documents&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a shard appears in the public tree, stop shipping the downloader and treat the path as a first-PR defect. Capture the listing in the ticket, but strip other apps' filenames before you paste anything into chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  First PR: move the handoff into app-private storage
&lt;/h2&gt;

&lt;p&gt;Your first PR should not add a new model flavor or a new prompt template. It should change the destination, the integrity check, and the rollback cleanup so reviewers can reason about filesystem side effects. Keep the diff small, and put the experiment steps into the pull-request template so the next junior repeats them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android destination
&lt;/h3&gt;

&lt;p&gt;Avoid public &lt;code&gt;DownloadManager&lt;/code&gt; destinations even when the sample looks shorter that way. Prefer internal storage or &lt;code&gt;getExternalFilesDir&lt;/code&gt;, which is removed on uninstall and stays out of the default Files view on stock Android. If you still need &lt;code&gt;DownloadManager&lt;/code&gt; for retries, point it at that private tree and hide the notification so the filename is not a user-visible attachment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed helper — unexecuted example, adapt to your module.&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;modelDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filesDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdirs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;enqueuePrivateDownload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DownloadManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAllowedOverMetered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNotificationVisibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DownloadManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VISIBILITY_HIDDEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDestinationInExternalFilesDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"models/$fileName"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// Never call setDestinationInExternalPublicDir(DIRECTORY_DOWNLOADS, fileName)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;dm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSystemService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DownloadManager&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;isCompleteModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectedSha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;digest&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MessageDigest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SHA-256"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputStream&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;buf&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;n&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;actual&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"%02x"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedSha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignoreCase&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you must use WorkManager instead of DownloadManager, write to the same private directory and delete the temp name in &lt;code&gt;finally&lt;/code&gt;. Do not let a retry promote a partial file into &lt;code&gt;MediaStore.Downloads&lt;/code&gt; because the worker lost its foreground window.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS destination
&lt;/h3&gt;

&lt;p&gt;Put weights in Application Support, mark the directory excluded from backup, and refuse to load a file that failed checksum after a kill. Documents looks convenient during a demo, and that convenience is how Files.app grows a copy of your speech or vision weights. Complete file protection is not a substitute for the backup exclusion flag, so you set both.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed helper — unexecuted example.&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;modelDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applicationSupportDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userDomainMask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;appropriateFor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withIntermediateDirectories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLResourceValues&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isExcludedFromBackup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;mutable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;mutable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setResourceValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;persistModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;expectedSHA256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nf"&gt;modelDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tmp"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completeFileProtection&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SHA256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"%02x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expectedSHA256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowercased&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceItemAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;withItemAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  React Native glue
&lt;/h3&gt;

&lt;p&gt;Juniors often inherit &lt;code&gt;react-native-fs&lt;/code&gt; samples that default to the downloads collection because those samples were written for user-picked photos. Pin the path to &lt;code&gt;DocumentDirectoryPath&lt;/code&gt; on iOS and to internal files on Android, then promote a &lt;code&gt;.tmp&lt;/code&gt; file only after the hash matches. Set &lt;code&gt;background: false&lt;/code&gt; on this first PR unless you already have a tested background transfer policy for killed downloads.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-native-fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DocumentDirectoryPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/models`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;downloadModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expectedSha256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelDir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;modelDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tmp`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downloadFile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;fromUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;discretionary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`download failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;expectedSha256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checksum mismatch; refusing to promote shard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;moveFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Flutter path on the same first PR
&lt;/h3&gt;

&lt;p&gt;Flutter teams hit the same handoff with &lt;code&gt;path_provider&lt;/code&gt; when a tutorial writes weights into &lt;code&gt;getDownloadsDirectory()&lt;/code&gt;. Use &lt;code&gt;getApplicationSupportDirectory()&lt;/code&gt; for the durable model, write a sibling &lt;code&gt;.tmp&lt;/code&gt;, and delete that sibling if the isolate dies. Do not request photos or media permissions for a file the UI never shows to the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Proposed helper — unexecuted example.&lt;/span&gt;
&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;modelFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;support&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;getApplicationSupportDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${support.path}&lt;/span&gt;&lt;span class="s"&gt;/models'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;recursive:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${dir.path}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;$name&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First rollback: leftover shards are the real regression
&lt;/h2&gt;

&lt;p&gt;A config rollback that only swaps a remote URL is incomplete for an on-device packager. You also need to refuse truncated files and delete temp names that the previous build created in private storage. Public leftovers are worse, because MediaStore keeps serving them after your feature flag goes dark.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rollbackModels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keepName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelDir&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readDir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelDir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.tmp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.part&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;keepName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RNFS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the first-hour experiment again after that rollback lands on the same device. Confirm Files and MediaStore still show nothing for the old name, even when you search by extension only. Confirm the app does not load &lt;code&gt;keepName&lt;/code&gt; if the checksum no longer matches the rolled-back manifest. Confirm a killed download does not leave a world-readable notification attachment with the model filename still visible.&lt;/p&gt;

&lt;p&gt;If the old shard silently remains, that is a ship blocker even when the chat UI looks healthy. Write the listing into the rollback ticket, and keep the PR focused on deletion plus the load-time checksum rather than on a new model architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the downloader without pasting device logs
&lt;/h2&gt;

&lt;p&gt;Onboarding week is when people paste &lt;code&gt;adb logcat&lt;/code&gt; and full path dumps into a public chat window. Those dumps include install paths, account names, and sometimes the model URL with a query token still attached. You want a second reader on the storage helper, not a copy of the bugreport traveling through someone else's retention policy.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source coding assistant with free model access and a free server option, which is enough for a junior to review a redacted downloader on a workspace you control. Paste the destination helper, the checksum function, and the rollback cleaner. Do not paste sysdiagnose output, Logcat, or a raw &lt;code&gt;content query&lt;/code&gt; that includes other apps' files.&lt;/p&gt;

&lt;p&gt;A useful prompt for that review looks like the block below, and you should keep the device listing out of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are reviewing a mobile on-device model downloader.
Flag any path that lands in public Downloads, MediaStore, or user-visible Files.
Flag missing checksums, missing .tmp cleanup, and backup exclusion flags.
Assume Android 15 and iOS 18. Do not suggest broad storage permissions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assistant is a reviewer, not a substitute for the Files and &lt;code&gt;adb&lt;/code&gt; steps above. If the model names a public directory, you still run the search yourself on the handset you used for onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table for the first PR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal you see in the first hour&lt;/th&gt;
&lt;th&gt;Ship the PR?&lt;/th&gt;
&lt;th&gt;Required change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shard in &lt;code&gt;/sdcard/Download&lt;/code&gt; or Files&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Private dir plus deletion of public copies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.tmp&lt;/code&gt; next to a loadable model&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Refuse load until checksum matches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Broad media permission only for models&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Drop the permission; use app storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback leaves old URL file on disk&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Rollback job deletes non-keep names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private dir, checksum, no Files hit&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Add the experiment to the PR template&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the table during review so the discussion stays on filesystem side effects. Do not expand the PR into a cloud-versus-edge architecture debate while public shards still exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;p&gt;This approach is for apps that actually download or unpack on-device weights on phones you can hold. You should not treat the snippets as OEM-certified, and you should not copy them into a web-only agent that never writes a shard. Skip or adapt the workflow when any of the following is true for your repo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your model is compiled into the App Bundle or IPA and never hits the network after install.&lt;/li&gt;
&lt;li&gt;You already stream embeddings from a server and store nothing durable on the device.&lt;/li&gt;
&lt;li&gt;Your compliance team requires MDM-managed volumes that this article does not describe.&lt;/li&gt;
&lt;li&gt;You cannot reproduce Files or MediaStore access on the training devices you own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article does not claim battery numbers, download speeds, or that every Android skin hides &lt;code&gt;getExternalFilesDir&lt;/code&gt; equally well. Samsung, Xiaomi, and other OEM Files apps can still surface app-specific external files after a search. Re-run the listing on the device class your users actually hold, and record that OEM name beside the OS version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to send back
&lt;/h2&gt;

&lt;p&gt;If you run the experiment, reply with the device, OS, framework version, and the lifecycle transition you used. Say whether the leftover shard recovered, restarted from zero, or silently remained after the first rollback. That single-device note is more useful than a generic mobile-AI architecture thread, and it tells the next junior whether your private-directory handoff actually survived process death.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>mobile</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
