Preserve original file name on API 33+ by switching to SAF picker#65
Preserve original file name on API 33+ by switching to SAF picker#65IgitBuh wants to merge 1 commit into
Conversation
The Photo Picker (ActivityResultContracts.PickVisualMedia) returns
synthetic, numeric DISPLAY_NAME values like "1000040501" on Android 13+
for privacy reasons. The recently added "preserve original file name"
feature then produced output filenames like "1000040501_Compressed.mp4"
instead of e.g. "PXL_20260316_124917401~2_Compressed.mp4".
Switch to ActivityResultContracts.OpenDocument(arrayOf("video/*")). SAF
URIs expose the real DISPLAY_NAME, require no extra permissions and work
uniformly across API levels.
While here, harden the surrounding code:
- Extract DISPLAY_NAME independently of MediaMetadataRetriever so a
thrown setDataSource/extractMetadata call (HDR, exotic codecs) no
longer silently discards the original name.
- Use a specific projection and .use { } for the cursor instead of a
null projection with manual close.
- Reject obviously synthetic numeric IDs as a defense in depth against
any other provider that may return one.
- Sanitize filesystem-illegal characters from the base name.
- Unify cache and gallery output naming through a single helper so the
fallback never produces the previous "Compressed_<ts>_Compressed.mp4"
double marker.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| // Reject Photo Picker synthetic IDs (e.g. "1000040501") — these are MediaStore IDs, | ||
| // not real filenames. The picker hides the actual name for privacy on Android 13+. | ||
| if (raw.isNullOrBlank() || raw.matches(Regex("""^\d{6,}$"""))) return null | ||
| return raw |
There was a problem hiding this comment.
| // Reject Photo Picker synthetic IDs (e.g. "1000040501") — these are MediaStore IDs, | |
| // not real filenames. The picker hides the actual name for privacy on Android 13+. | |
| if (raw.isNullOrBlank() || raw.matches(Regex("""^\d{6,}$"""))) return null | |
| return raw |
Please remove this, it's not needed
| // Resolve the display name independently — if metadata extraction throws below | ||
| // (HDR videos, unsupported codecs, slow URIs), we still keep the original name. | ||
| val originalName = queryDisplayName(context, uri) | ||
|
|
| try { | ||
| context.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) | ||
| } catch (_: SecurityException) { | ||
| // Some providers don't grant persistable URIs; the URI is still readable for this session. |
There was a problem hiding this comment.
???
Is this tested? There shouldn't be any SecurityExceptions
|
Upon further consideration, I will not be merging this PR. Swapping the modern PickVisualMedia for SAF is a massive downgrade to the User Experience. SAF is designed for document management, not media browsing. It lacks the large thumbnails, album sorting, and smooth performance of the Photo Picker. On many OEM skins (like Samsung), SAF defaults to a clunky list view that makes finding specific videos extremely frustrating for average users. I am not willing to ruin the core video selection experience for 40k+ users just to bypass Android 13's privacy design and retrieve an original filename string. I also do not want to merge a 100% AI generated pull request. I appreciate any time you put into this, but I'm going to close this. |
|
Thanks for the review, all three points are fair.
The PR was assisted by Claude Code (footer in the description) but I did test it end-to-end on my Pixel device (Android 16, API 36): real video picked via the new SAF picker, compressed, saved to gallery, filename comes out as the real PXL_*_Compressed.mp4. |
Summary
The recently added "preserve original file name" feature produces filenames like
1000040501_Compressed.mp4instead of e.g.PXL_20260316_124917401~2_Compressed.mp4when the source video is selected through the in-app Photo Picker. This PR fixes that by switching the picker to SAF (ActivityResultContracts.OpenDocument).Root cause
ActivityResultContracts.PickVisualMedia()on Android 13+ returns picker URIs of the formcontent://media/picker/0/com.android.providers.media.photopicker/media/<ID>. QueryingOpenableColumns.DISPLAY_NAMEon these URIs returns the picker's synthetic numeric ID, not the real filename — by design, for privacy. The previous version then used that numeric ID as the "original name", producing the1000040501_Compressed.mp4style output observed on a Pixel 8 Pro (Android 16).Looking it up confirmed the suspicion: MediaStore ID
1000040501corresponds to a real videoPXL_20260316_124917401~2.mp4. The app has noREAD_MEDIA_VIDEOpermission (intentionally — see README "no invasive permissions"), so it cannot resolve the ID back to the real name itself.Fix
PickVisualMediawithOpenDocument(arrayOf("video/*")). SAF returns the realDISPLAY_NAME, requires no extra permissions, and works uniformly across API levels.Bonus hardening (same area, low risk)
These were noticed while tracing the bug and are included to make the feature resilient to a few related edge cases:
DISPLAY_NAMEbefore theMediaMetadataRetrieverblock, so a thrownsetDataSource/extractMetadata(HDR videos, unusual codecs) no longer silently discards the original name..use { }for the cursor (was anullprojection with manualclose(); cursor was leaked on empty/exception paths).DISPLAY_NAMEs (^\d{6,}$) as a defense in depth./ \ : * ? " < > | \x00–\x1F).Compressed_<ts>_Compressed.mp4double marker.Test plan
_Compressed.mp4(e.g.PXL_20260316_124917401~2_Compressed.mp4).🤖 Generated with Claude Code