Android content URIs are one of those things that you may not get right if you have just developed your apps using Cordova and you are not familiar with the Android Platform APIs. I'd like to share with you my experience dealing with paths on Android to hopefully prevent some headaches.

Something I've learned by working on Cordova projects is that you can't just rely on plugins to (always) work properly. The community creates very awesome and useful plugins, but sometimes developers don't have enough time to keep plugins up to date and Android moves fast. On every new release, Android introduces new APIs and some others become obsolete.

Displaying a file browser and getting metadata from the file you select is not a trivial task at first. If you've worked on a Cordova project that needed to read files on Android, you may have used the official Cordova File Plugin. For that purpose, the resolveLocalFileSystemURL method takes an absolute file path and returns a File object, but it'll fail to resolve file paths that you pick from a file browser on Android 4.4 KitKat and above.

Reading files on Android

The official Cordova File Plugin is the best option to handle files on Android, it implements the HTML5 File API and enables you to read/write files in your device, so you can access the file metadata from your JavaScript code.

If you want to access a file using this plugin, you'll simply have to call resolveLocalFileSystemURL with the path of the file and it will return a FileEntry.

const filePath = "file:///storage/emulated/0/Download/cat.jpg"

window.resolveLocalFileSystemURL(filePath, fileEntry => {
  fileEntry.file(file => {
    const { name } = file;
    // name => "cat.jpg"
  });
}, error => console.error(error));

Before Android 4.4, file browsers (like the Android Photo Gallery) returned paths that can be handled by resolveLocalFileSystemURL, but now content provider based paths are returned.

Content paths in Android 4.4 and beyond

With Android 4.4 KitKat (API level 19), Google introduced the Storage Access Framework (SAF), a better way to share, browse and access files accross apps and providers. SAF also included a new UI, which connects with all the apps that provide a document provider and lets you access documents, images and recently opened files in a consistent way accross different apps.

If you want to launch an activity on Android, you have to use an Intent. It's not the goal of this post to talk deeper about Intents (if you're interested, you can check [1], [2], [3], which are awesome resources), but I do want to mention that a commonly used Intent to select data from your device is ACTION_GET_CONTENT. On Android 4.4, SAF was integrated with the ACTION_GET_CONTENT intent in a way that when an app starts the action, the new file picker UI is opened and files from other content providers are displayed.

The cordova-filechooser plugin uses the ACTION_GET_CONTENT intent to select files from your app. The URI that this plugin returns when you select a file used to be the absolute path of the file:

content://media/external/images/media/3951

But with SAF, the UI will be relative to its content provider. This is an example of a file returned by this plugin when you select a file from the Downloads folder:

content://com.android.providers.downloads.documents/document/639

Note that this is not an absolute path, it's now relative to the content provider the file belongs (com.android.providers.downloads.documents).

Then you have to resolve this path into an absolute path, that can be used by resolveLocalFileSystemURL to retrieve the FileEntry.

So, how can we handle those paths?

Luckily, we can use cordova-plugin-filepath to convert paths that are relative to content providers into absolute paths. This new path can now be used in resolveLocalFileSystemURL to get the file object. Here's how we do it in Semaphor:

window.fileChooser.open(contentURI => {
  window.FilePath.resolveNativePath(contentURI, absolutePath => {
    window.resolveLocalFileSystemURL(absolutePath, fileEntry => {
      fileEntry.file(file => {
        const { name, size } = file;
        console.log(`The name of this file is ${name} and its size is ${size}`);
      });
    }, error => console.error(error));
  }, error => console.error(error));
});

So, basically, if resolveLocalFileSystemURL doesn't work for you, take a look at the path you are passing to it. If it turns out it's a content URI, then you'll first need to use FilePath.resolveNativePath to get the absolute path.

I hope you found this article useful. Thanks for reading!