The Death of External Storage: How Can I Stay Away From Files?

Under the fairly innocuous title of “scoped storage”, Google has announced that external storage, as Android developers have used it, effectively is dead, for Android Q and onwards.

I am going to spend some time on this over the course of the week, as this change needs some attention:

Monday, I described what has changed and what you are supposed to use.

Yesterday, I covered the limited options for actually getting a file on external or removable storage.

Today, I will review how you might still wind up with references to files… that you cannot access

One of the salvos in the War on Files came in Android 7.0, with the introduction of FileUriExposedException . This would be raised if you put a file:// Uri into an Intent and tried using that Intent to start an activity, etc. It would crash the app and steer you towards using FileProvider to make that content available to other apps.

The hope was that this would limit the number of file:// Uri values flitting through the OS, and I am sure that it helped. However:

It does not come into play for apps with a targetSdkVersion below 24, so legacy apps that are not getting updates will still emit file:// Uri values

I am sure that there are creative ways of passing a Uri around that bypass those checks

FileUriExposedException is thrown by StrictMode , and so lots of people suggest that you simply disable that check to get past the “problem”

Most likely, in a world with READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE , those file:// Uri values would work, assuming that your app held the appropriate permissions. On Android Q, you cannot hold those permissions. It is very unlikely that a file:// Uri that another app could use (to create it) will be one that your app can use (to consume it). In other words, if you get a file:// Uri , you will probably crash when you go to open that file for reading or writing.

Sometimes, an <intent-filter> controls what Uri values we receive. For example, if you have an activity that supports ACTION_VIEW , you will have an <intent-filter> in your manifest that, among other things, declares what sorts of content you can accept, through <data> elements.

For example, you might have an <activity> like this in your manifest:

<activity android:name= ".MainActivity" > <intent-filter> <action android:name= "android.intent.action.MAIN" /> <category android:name= "android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name= "android.intent.action.VIEW" /> <category android:name= "android.intent.category.DEFAULT" /> <category android:name= "android.intent.category.BROWSABLE" /> <data android:scheme= "file" /> <data android:scheme= "https" /> <data android:scheme= "content" /> <data android:mimeType= "text/plain" /> </intent-filter> </activity>

Here we have a <data android:scheme="file" /> element advertising support for the file scheme.

You do not want those <data android:scheme="file" /> elements on Android Q, as most likely you will be unable to use the Uri that you receive.

What you can do is set up an <activity-alias> for those:

Create an <activity-alias> pointing to the affected activity

Clone the relevant <intent-filter> elements from the <activity> onto the <activity-alias>

Remove the <data android:scheme="file" /> elements from the <activity>

Remove the other schemes from the <activity-alias> , leaving only the <data android:scheme="file" /> for declaring schemes

Define a boolean resource, one that is true by default but false in a res/values-v29/ edition

Use that boolean resource in android:enabled on the <activity-alias>

You will wind up with a paired <activity> and <activity-alias> like this:

<activity android:name= ".MainActivity" > <intent-filter> <action android:name= "android.intent.action.MAIN" /> <category android:name= "android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name= "android.intent.action.VIEW" /> <category android:name= "android.intent.category.DEFAULT" /> <category android:name= "android.intent.category.BROWSABLE" /> <data android:scheme= "https" /> <data android:scheme= "content" /> <data android:mimeType= "text/plain" /> </intent-filter> </activity> <activity-alias android:name= ".FileAlias" android:enabled= "@bool/supportFileScheme" android:targetActivity= ".MainActivity" > <intent-filter> <action android:name= "android.intent.action.VIEW" /> <category android:name= "android.intent.category.DEFAULT" /> <category android:name= "android.intent.category.BROWSABLE" /> <data android:scheme= "file" /> <data android:mimeType= "text/plain" /> </intent-filter> </activity-alias>

The net result is that the <activity-alias> will be used on older devices, where you can still consume the file:// Uri values. But that <activity-alias> will be disabled on Android Q and above. This helps reduce the number of unusable Uri values that your app will get.

You might wonder if Android Q would do this sort of thing automatically, basically ignoring <data android:scheme="file" /> elements in manifests. At least as of Q Beta 1, it does not, though it is possible it might get added in later releases.

Unfortunately, there are other ways that you can wind up with a Uri from another app that are not controllable via <intent-filter> by scheme.

The big one is ACTION_SEND . For reasons that were never clear to me, ACTION_SEND does not work like ACTION_VIEW and other Uri -centric Intent actions. While you can use <data> to filter by MIME type, AFAIK scheme and related restrictions are ignored. As a result, you can wind up with EXTRA_STREAM values that contain file:// Uri values.

There other similar sources of Uri , like ACTION_SEND_MULTIPLE and the clipboard, that suffer from a similar lack of scheme-based filtering.

IMHO, the best solution is, if the scheme is file and you are on Android Q or higher, to detect that combination of conditions and display a dedicated error UI. You can explain that the other app is out of date or has a bug and is incompatible with your app. Try to give the user an understanding of what is causing the problem (sending you stuff from this older app) and how to get past it (switch to a newer app).

At worst, make sure that you are cleanly handling any IOException that might come from trying to use that Uri (e.g., from ContentResolver and openInputStream() ). Do not just say “oh, we’ll let ACRA/Crashlytics/Firebase Crash Reporting/whatever handle it”, as inevitably you wind up showing some generic “oops!” error message. Leave that for the “unknown unknowns”, where you cannot predict the problem in advance. For something like this, try to offer an error message that is more specific and more actionable.

Tomorrow, I’ll review some reasons why all of this may be happening.

Find out about new posts on the CommonsBlog via the Atom feed, or follow @CommonsWare on Twitter!

— Mar 27, 2019