Skip to content

Implement directory picking and related functions #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

amake
Copy link
Contributor

@amake amake commented Apr 13, 2021

Note: This PR includes the changes in #19, so you might want to review that one first.

This PR addresses #16 by adding the following functions:

  • FilePickerWritable.openDirectory
  • FilePickerWritable.getDirectory
  • FilePickerWritable.resolveRelativePath
  • FilePickerWritable.isDirectoryAccessSupported

Please see the dartdoc comments of each for details.

To represent the result of picking a directory or resolving a relative path to a directory, I have introduced the DirectoryInfo class. Because the result of resolveRelativePath could be either a file or a directory, I have moved the bulk of FileInfo into a new abstract class EntityInfo from which both FileInfo and DirectoryInfo inherit (both classes have the same properties for now). See #16 (comment) for additional discussion of this design decision.

Android details

Android only supports obtaining persistable directory access on API 21 (Android 5 "Lollipop") and later, but this plugin supports back to API 19. I have maintained compatibility with this PR; users are expected to use isDirectoryAccessSupported to check if directory access is supported before calling any of the other new functions.

Another wrinkle is that one of the core primitives required to implement getDirectory (and resolve .. in resolveRelativePath), DocumentsContract#findDocumentPath, is only available on API 26 (Android 8 "Oreo"). To support getDirectory on prior OSes I implemented a potentially very costly breadth-first recursive search through the filesystem. It works, but the experience could be quite poor if the user has a lot of files, or if the root is far removed from the target file.

(On Android 7 and earlier you currently can't resolve .. above your start point. This could be improved, but I'm not sure how to make the API clear enough to be usable.)

Android URIs

To make sense of the Android implementation you need to know the following about Android Storage Access Framework URIs.

There are three kinds of URI that we care about:

  • "Document" URIs like content://com.android.externalstorage.documents/document/primary%3ADocuments%2Ffoo%bar.txt
    • This points at the document with ID Documents/foo/bar.txt in primary storage
    • This is what you get from the file picker if you pick the file Documents/foo/bar.txt
  • "Tree" URIs like content://com.android.externalstorage.documents/tree/primary%3ADocuments
    • This points at the directory tree with ID Documents in primary storage
    • This is what you get from the directory picker if you pick Documents
  • "Tree+Document" URIs like content://com.android.externalstorage.documents/tree/primary%3ADocuments/document/primary%3ADocuments%2Ffoo%bar.txt
    • This points to the document with ID Documents/foo/bar.txt under the directory tree Documents, both in primary storage
    • This kind of URI needs to be built via methods in DocumentsContract
    • This kind of URI is what you need to supply when accessing documents for which you don't have explicit permission, when they are children in a tree for which you do have permission: the tree part represents your access grant

Much of the code in the Android implementation concerns itself with the manipulation such URIs:

  • Ensuring we have the right kind of URI for a given query
  • Ensuring we return the right kind of URI from a query
  • Discovering the document ID for a given file

The above example URIs are taken from the local storage provider. While you can dissect and make sense of the parts of these URIs, so you might be tempted to try parsing them for easier manipulation, please note that there are various exhortations against doing so in the Android documentation:

Each document has a unique identifier within that provider. This identifier is an opaque implementation detail of the provider, and as such it must not be parsed.

(source)

Compatibility

Unfortunately directory access is not widely supported by third-party apps. The only sources that work are:

  • Android
  • iOS
    • Local storage
    • iCloud Drive

Apps confirmed to not support directory access include Dropbox, Google Drive, and FileBrowser.

Intended use case

I have implemented these features for use in my app, Orgro, which is a viewer for Org Mode files. My use case is:

  1. User selects a file with the existing file picker
  2. The file is analyzed to see if it has relative references to other files (images or links)
  3. If isDirectoryAccessSupported returns false then we quit here
  4. Otherwise, if the file does have relative links, the app checks to see if it already has persistent access to a directory that contains the opened file
    1. If the app already has access to an appropriate directory, then getDirectory is called to get an identifier for the directory containing the opened file
      1. Relative links are resolved against the directory identifier with resolveRelativePath
      2. The resulting file identifiers are opened with the existing readFile method
    2. If the app does not have access to an appropriate directory, then the user is prompted to select one with openDirectory
      • Go to (4) above

I have this implemented and tested on iOS 14, Android 11, and Android 6.

@amake amake force-pushed the directory-picker branch 3 times, most recently from 88b14f2 to 20bc343 Compare April 18, 2021 15:02
@amake
Copy link
Contributor Author

amake commented Apr 23, 2021

@hpoul Would it help if I broke this PR up into smaller ones?

amake added a commit to amake/orgro that referenced this pull request Apr 26, 2021
@amake amake force-pushed the directory-picker branch from 0844a79 to 966ce1a Compare August 24, 2021 11:46
The call to URL.bookmarkData() can fail with some file providers if the file is
e.g. online only and not yet materialized to the local device.
@amake
Copy link
Contributor Author

amake commented Sep 12, 2024

I have been actively using this branch in my app for years now. I will continue to maintain it. I just mention this to note that I am merging other, smaller PRs into it so if you ever do feel like looking at this one, I suggest you merge the others first.

@deakjahn
Copy link

@amake Does this actually mean that this plugin hasn't got any TLC since 2021? I'd suggest to start your own forked full package then and publish it normally, with clear conscience. Granted, we can link to GitHub but it would be better the usual way. I'd be one of your happy users. :-)

I'd also recommend then #46 to your attention, too.

@amake
Copy link
Contributor Author

amake commented Jan 11, 2025

As I understand it, this package is used in AuthPass, which is actively maintained. The author has been doing minimal maintenance on it as required to keep it working (last commit in 2023) but I guess he considers it feature-complete.

I think the problem with this PR is that it's big and adds a lot of code to support a feature that the author isn't interested in. I've considered a full fork, but to be honest I don't want the support burden.

@deakjahn
Copy link

deakjahn commented Jan 12, 2025

I was planning to have a few remarks, suggestions and whatnot. :-) I can put all that I need into my own app for sure but at the same time, it would really be good to have one package that is both cross platform and supports the possibilities...

@deakjahn
Copy link

All the more that I can't find a mix of plugins that really work right now. :-) For instance, I used pick_or_save previously which is fine but Android-only. Still, I had a persistent grant in an app I'm testing in the emulator. Now I tried to move to yours, with the anticipation of adding my intended modifications. And I needed to wipe the emulator clean for unrelated reasons (it just refused to start any more, it happens painfully too often these days). And no I can't get the same permission with your plugin that I used to get with pick_or_save: No permission grants found for UID whatever and Uri... So, you do something differently than pick_or_save for sure. :-)

@hpoul
Copy link
Owner

hpoul commented Jan 13, 2025

Yeah, this PR adds quite a bit of code I'd have to look through.. and not sure if it makes too much sense adding code I've never actually used myself or verified that it's working.. it would be a bit easier to have smaller PRs.. 🤷

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.

3 participants