Skip to content

Browse the folders you have granted access to - #527

Draft
andiwand wants to merge 1 commit into
landing/04-direct-pickerfrom
landing/05-folder-browsing
Draft

Browse the folders you have granted access to#527
andiwand wants to merge 1 commit into
landing/04-direct-pickerfrom
landing/05-folder-browsing

Conversation

@andiwand

@andiwand andiwand commented Jul 26, 2026

Copy link
Copy Markdown
Member

Fifth of six, stacked. Base: landing/04-direct-picker (#526).

"See the files on my phone" has no permission behind it any more. READ_EXTERNAL_STORAGE has not reached documents since scoped storage, READ_MEDIA_* covers only images/video/audio, and Play restricts MANAGE_EXTERNAL_STORAGE to file managers and backup apps — a document viewer asking for it gets the listing rejected. The manifest still declares nothing but INTERNET, and it stays that way.

ACTION_OPEN_DOCUMENT_TREE does the same job with the user's consent instead: pick a folder once, we persist a read-only grant, and the landing screen lists and navigates it from then on. No write flag is ever requested.

  • DocumentsContract, not androidx DocumentFile — the latter issues a fresh query per child the moment a name or type is read. One cursor covers a whole folder.
  • SupportedDocumentTypes has to guess, and says so. MetadataLoader only knows what is supported after libmagic has run over the cached file; a folder listing has a name and whatever mime type the provider volunteered — regularly application/octet-stream, hence the extension fallback. It mirrors the STRICT_CATCH intent filter and nothing keeps the two in step, so both sides now cross-reference each other, as does CLAUDE.md.
  • prune() learns the tree prefix rule. A document below a granted tree is covered by that tree's grant, and its uri spells the tree out — so nothing has to record where a document was opened from, and FileLoader.Options needs no new field.
  • The tree grant goes through PersistedUriPermissions, not the resolver. addFolderTree writes the tree down on the view model's executor, so a prune() in that window would find a grant nothing points at yet and hand it straight back. That is the same race Hold on to a grant until the recent list names it #540 closed for the recent list, and taking the grant through the shared helper puts the folder in the same in-flight set. prune()'s decision is now isReferenced(), which takes strings rather than a Context so a plain JVM test covers it, with the release half lifted out beside it.

Verification

20 instrumented tests green, and PersistedUriPermissionsTest now covers the tree prefix rule (including a sibling tree that merely shares a prefix). On a device, through the real DocumentsUI picker: granted Documents, browsed into it and into a subfolder, confirmed an .mp3 was filtered out and directories sorted first, back went up a level rather than out of the app, opened a document from the folder and it landed in RECENT — which is the isRetained fallback doing its job, since takePersistableUriPermission throws on a tree-child uri.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LrDgmtcnqm8xEUtt42cTDd

"See the files on my phone" has no permission behind it any more.
READ_EXTERNAL_STORAGE has not reached documents since scoped storage,
READ_MEDIA_* covers only images, video and audio, and Play restricts
MANAGE_EXTERNAL_STORAGE to file managers and backup apps - a document
viewer asking for it gets the listing rejected. The manifest still
declares nothing but INTERNET, and it stays that way.

ACTION_OPEN_DOCUMENT_TREE does the same job with the user's consent
instead: they pick a folder once, we persist a read only grant, and the
landing screen lists and navigates it from then on. Read only - no write
flag is ever requested.

Listing goes through DocumentsContract rather than androidx's
DocumentFile, which issues a fresh query per child the moment a name or
type is read. One cursor covers a whole folder.

SupportedDocumentTypes decides what is worth showing. It has to guess:
MetadataLoader only knows what is supported after libmagic has run over
the cached file, and a folder listing has nothing but a name and
whatever mime type the provider volunteered - which is regularly
application/octet-stream, hence the extension fallback. It mirrors the
STRICT_CATCH intent filter and nothing keeps the two in step, so both
sides now say so, as does CLAUDE.md.

prune() learns that a document below a granted tree is covered by that
tree's grant, which it can tell from the uri alone - so nothing has to
record where a document was opened from.

The tree grant is taken through PersistedUriPermissions rather than the
resolver directly, so that it joins the in flight set the recent list's
grants already use: addFolderTree writes the tree down on the view
model's executor, and a prune in that window would find a grant nothing
points at yet and hand it straight back. Same race, same fix, one folder
instead of one document.

That decision is now isReferenced(), which takes the strings rather than
a context and so can be covered by a plain jvm test, with the release
half lifted out beside it to keep prune() down to the loop it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiSZ4FeptMUsFjAuCewzQZ
@andiwand
andiwand force-pushed the landing/04-direct-picker branch from 9ace181 to 870bcce Compare July 27, 2026 22:01
@andiwand
andiwand force-pushed the landing/05-folder-browsing branch from 982f294 to f007dab Compare July 27, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant