I added video sharing to an Android app. It writes an mp4 into the cache, wraps it in a FileProvider URI, and hands it to the system share sheet.
The temp files are 1–6 MB, so I added cleanup:
context.startActivity(Intent.createChooser(send, null))
shareVm.consume() // clears state and calls videoFile.delete()We shared it, so delete it. Reads naturally. I had verified the renderer on a real device, confirmed the mp4 was produced, and this went to production.
What was wrong
startActivity(createChooser(...)) only presents the picker.
The receiving app opens the FileProvider URI after the user picks a target. That gap is a second at best, and several while someone scrolls a list of apps. I deleted the file inside that window.
When the receiving app opens the URI it gets a FileNotFoundException — in its own process. Your app is told nothing. No crash, no log, just a share that arrives without the video.
Why it survived verification
My on-device check ended here:
val file = TerritoryReelRenderer(ctx).render(payload) { … }
assertTrue(file!!.length() > 200_000)
assertTrue(String(head, 4, 4) == "ftyp")I confirmed the renderer produces a file. I never confirmed that the file survives the trip through an intent into another app. My verification stopped at precisely the point where the bug begins.
Pause for a moment — where would you move that cleanup?
There is nowhere to clean up
iOS has an answer: delete in the share sheet's onDismiss. The sheet closing means the file is no longer needed.
Android has no such callback. The chooser tells you neither what was picked nor when it ended. startActivityForResult doesn't help either — a share target is under no obligation to return a result.
So deleting "when the share finishes" is not something you can express. You have to move the moment instead.
/** Sweeps old scratch clips when the *next* render starts — the only point
* that can never race the chooser still holding the current file. */
private fun sweepOldClips() {
val cutoff = System.currentTimeMillis() - 60 * 60 * 1000
File(context.cacheDir, "share").listFiles()
?.filter { it.name.startsWith("plotta-reel-") && it.lastModified() < cutoff }
?.forEach { it.delete() }
}The previous share is guaranteed finished; the current one hasn't started. Files linger in the cache for an extra hour, which is exactly what a cache is for.
One more, from the same file
The renderer wrote its mp4 to the root of context.cacheDir, while file_paths.xml said:
<paths>
<cache-path name="share" path="share/" />
</paths>Only cache/share/ is exposed. A file written to the root gets no URI from FileProvider at all. Same shape of bug: fine through rendering, broken only at the hand-off.
Both failures share a property. Producing the file succeeded; passing it on failed. A success signal guaranteeing nothing about the outcome is a story I've written before — every job was green for two weeks, and the site was stale for two weeks.
Three things to check
- Are you reading
startActivityas "done"? It means "presented". Treat every line of cleanup after it as suspect. - Is the file you hand to
FileProviderinside a path declared infile_paths.xml? Write it to the root and rendering works while sharing quietly doesn't. - Where does your on-device verification stop? At the file being created, or at another app opening it? The bug lives between those two.
The honest part
This shipped. Users tapped share and got no video. A post-release code review caught it and the next version fixed it.
And the reason for the cleanup was right — nobody deleting 1–6 MB files means the cache grows forever. Exactly one thing was wrong: when it deleted. Necessary code in the wrong place is the hardest kind to spot, because nothing about the code looks strange.
Is something in your app deleting a temp file before its consumer is done with it?