Skip to content

Preserve original file name on API 33+ by switching to SAF picker#65

Closed
IgitBuh wants to merge 1 commit into
JoshAtticus:mainfrom
IgitBuh:fix-preserve-original-filename
Closed

Preserve original file name on API 33+ by switching to SAF picker#65
IgitBuh wants to merge 1 commit into
JoshAtticus:mainfrom
IgitBuh:fix-preserve-original-filename

Conversation

@IgitBuh
Copy link
Copy Markdown

@IgitBuh IgitBuh commented May 12, 2026

Summary

The recently added "preserve original file name" feature produces filenames like 1000040501_Compressed.mp4 instead of e.g. PXL_20260316_124917401~2_Compressed.mp4 when 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 form content://media/picker/0/com.android.providers.media.photopicker/media/<ID>. Querying OpenableColumns.DISPLAY_NAME on 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 the 1000040501_Compressed.mp4 style output observed on a Pixel 8 Pro (Android 16).

Looking it up confirmed the suspicion: MediaStore ID 1000040501 corresponds to a real video PXL_20260316_124917401~2.mp4. The app has no READ_MEDIA_VIDEO permission (intentionally — see README "no invasive permissions"), so it cannot resolve the ID back to the real name itself.

Fix

  • Replace PickVisualMedia with OpenDocument(arrayOf("video/*")). SAF returns the real DISPLAY_NAME, requires no extra permissions, and works uniformly across API levels.
  • Take a persistable URI permission so the picked URI survives configuration changes.

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:

  • Extract DISPLAY_NAME before the MediaMetadataRetriever block, so a thrown setDataSource/extractMetadata (HDR videos, unusual codecs) no longer silently discards the original name.
  • Use a specific projection and .use { } for the cursor (was a null projection with manual close(); cursor was leaked on empty/exception paths).
  • Reject obviously synthetic numeric DISPLAY_NAMEs (^\d{6,}$) as a defense in depth.
  • Sanitize filesystem-illegal characters from the base name (/ \ : * ? " < > | \x00–\x1F).
  • Unify cache file and gallery save naming through a single helper, so the fallback never produces the previous Compressed_<ts>_Compressed.mp4 double marker.

Test plan

  • Build and install on Pixel 8 Pro (Android 16, API 36).
  • Pick a video via the new picker → output filename is the real source name plus _Compressed.mp4 (e.g. PXL_20260316_124917401~2_Compressed.mp4).
  • No new permissions requested; existing share-intent path unchanged.
  • Behavior on API 28- (legacy SAF save flow) — unchanged code path; not retested.

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown
Owner

@JoshAtticus JoshAtticus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see suggested changes

Please also confirm you have tested and upload a video of the testing since this PR is 100% AI generated

Comment on lines +435 to +438
// 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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded linebreak

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.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

Is this tested? There shouldn't be any SecurityExceptions

@JoshAtticus
Copy link
Copy Markdown
Owner

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.

@JoshAtticus JoshAtticus added the wontfix This will not be worked on label May 12, 2026
@IgitBuh
Copy link
Copy Markdown
Author

IgitBuh commented May 12, 2026

Thanks for the review, all three points are fair.

  1. Synthetic-ID rejection: agreed, with the SAF picker it's defensive code for an unlikely share-intent edge case. Will remove.
  2. Linebreak: fixed.
  3. takePersistableUriPermission: you're right, the persistable grant isn't actually needed here (the URI is only used within the activity session). Removing the call entirely rather than just the try/catch.

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.

Repository owner locked as resolved and limited conversation to collaborators May 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

wontfix This will not be worked on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants