hpoul / file_picker_writable

Flutter plugin to choose files which can be read, referenced and written back at a later time.
https://pub.dev/packages/file_picker_writable
MIT License
17 stars 12 forks source link

Implement directory picking and related functions #20

Open amake opened 3 years ago

amake commented 3 years ago

~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:

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 https://github.com/hpoul/file_picker_writable/issues/16#issuecomment-815359997 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:

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

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:

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 commented 3 years ago

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

amake commented 2 months ago

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.