DEV Community

Roronoa
Roronoa

Posted on

Keep First-Hour Model Weights Out of Shared Storage After a Rollback

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.

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.

Why a rollback is not a cleanup

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 .bin or .task 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.

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 cache/, Files/, 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.

What usually survives the first revert

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.

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

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.

Record the environment before anyone hits download

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.

Copy this block into the pull request and fill it on the device, not from memory after lunch.

# 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 -> Home / app switcher -> 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
Enter fullscreen mode Exit fullscreen mode

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.

Keep hour-one iteration off the phone

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.

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 SharedPreferences, or in a committed local.properties file. This article does not claim model names, quotas, hardware, uptime, or permanence for that hosted option.

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.

// Proposed Android client: hour-one traffic talks to a revocable host.
// Do not ship this token in the APK. Inject it from CI or a local untracked file.
data class HourOneEndpoint(
    val baseUrl: String,          // development host, not production inference
    val token: String             // memory only; never log the value
)

fun shouldDownloadOnDeviceModel(contractFrozen: Boolean, prAddsPackagedWeights: Boolean): Boolean {
    return contractFrozen && prAddsPackagedWeights
}
Enter fullscreen mode Exit fullscreen mode

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

Artifact: a rollback storage audit you can run in one sitting

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.

Decision table

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

Print the table in the PR with a third column filled from adb 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.

Android: pin weights to private cache

// Proposed helper. Treat as unexecuted sample until you run it on the device card.
fun modelCacheFile(context: Context, name: String): File {
    val dir = File(context.cacheDir, "on-device-models").apply { mkdirs() }
    require(!name.contains("..")) { "refusing path traversal in model name" }
    return File(dir, name)
}

fun assertNotShared(file: File, context: Context) {
    val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
    check(!file.absolutePath.startsWith(downloads.absolutePath)) {
        "first-hour model escaped into Downloads"
    }
    check(file.absolutePath.startsWith(context.cacheDir.absolutePath)) {
        "first-hour model is not in app-private cache"
    }
}
Enter fullscreen mode Exit fullscreen mode
# Proposed audit after you background the app and revert the PR.
# Replace the package name with the sample you actually installed.
adb shell am force-stop com.example.voiceapp
adb shell run-as com.example.voiceapp ls -la cache/on-device-models || true
adb shell content query --uri content://media/external/file \
  --projection _display_name:_data \
  --where "_display_name LIKE '%.tflite' OR _display_name LIKE '%.bin'"
Enter fullscreen mode Exit fullscreen mode

If content query 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.

iOS: exclude caches from backup

// Proposed helper. Unexecuted until you run it on the iOS device listed on the card.
func modelCacheURL(filename: String) throws -> URL {
    let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
    let dir = caches.appendingPathComponent("on-device-models", isDirectory: true)
    try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
    var url = dir.appendingPathComponent(filename)
    var values = URLResourceValues()
    values.isExcludedFromBackup = true
    try url.setResourceValues(values)
    return url
}
Enter fullscreen mode Exit fullscreen mode

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 getApplicationDocumentsDirectory() dumped into a shareable folder.

Proposed first-hour sequence

Run this as a single-device experiment. Stop if any step needs a second phone or a production key.

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

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.

Limitations and who should not use this

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.

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.

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.

What to send back if you run it

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.

Top comments (0)