The Death of External Storage: The End of the Saga(?)

If Q Beta 4 really does have the final APIs, then we may now have the final implementation of scoped storage. While external storage as we know it is still going away, it will not be for a while, and the user experience should be reasonable.

So, let’s review where we are now, with another set of fictionally-asked questions (FAQs):

What Are the Options?

Apps can either have normal or legacy storage.

With legacy storage, everything behaves as it did in Android 4.4 through 9.0:

  • You can use getExternalFilesDir() and similar directories without permissions

  • You can work with the rest of external storage if you hold READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE

Without legacy storage, apps still can use getExternalFilesDir() and similar directories without permissions. However, the rest of external storage appears to be inaccessible via filesystem APIs. You can neither read nor write. This includes both files created by other apps and files put on the device by the user (e.g., via a USB cable).

It is conceivable that there are some types of content that are still visible via the filesystem, as the documentation has:

An app that has a filtered view always has read/write access to the files that it creates, both inside and outside its app-specific directory

So, I cannot rule out scenarios where the app can work outside of getExternalFilesDir() and similar directories via the filesystem APIs. I just have not found one yet.

What Changed From Q Beta 3?

Q Beta 3 also had two modes: legacy and sandboxed. Apps with sandboxed external storage could read and write everywhere on external storage… because they were not working with the real external storage. Instead, they would read and write from a sandbox. While this allowed existing code to keep working, it was costly from a user experience standpoint, as many users would not know to wander into the Android/sandboxes/ directory to find an app’s sandboxed edition of external storage.

Now, instead of apps having a “sandboxed” separate bit of external storage, they have a “filtered” view of the real external storage.

What Changed From Q Beta 1 and 2?

Too much changed to list here. Can’t we focus on more pleasant topics?

What is the User Experience?

Whether apps have normal or legacy external storage does not matter to the user, to a large degree. Files show up wherever they would have shown up originally. This is particularly important for apps with a lower targetSdkVersion that may never get updated — users can use those apps the same way they have for years.

What Am I Supposed to Use, Then?

You are welcome to continue using getExternalFilesDir(), getExternalCacheDir(), getExternalMediaDirs(), getExternalCacheDirs(), and getExternalFilesDirs(), if you were using those before.

However, the Storage Access Framework (e.g., ACTION_OPEN_DOCUMENT) is the primary way that apps should work with user-supplied content.

Apps that have a focus on media — audio, video, and images — can use the MediaStore. Note, though, that you need READ_EXTERNAL_STORAGE to be able to see other apps’ content in the MediaStore.

One thing that you are not supposed to use is an <intent-filter> supporting the file scheme. You will not be able to read files written by other apps, so if you get a Uri like that, probably it is useless to you. The technique that I wrote about previously, to use <activity-alias> to support file only on older devices, should still work.

I Don’t Like Change — How Do I Stick With What Worked Before?

For Android Q, you can add android:requestLegacyExternalStorage="true" to your <application> element in the manifest. This opts you into the legacy storage model, and your existing external storage code will work.

Technically, you only need this once you update your targetSdkVersion to 29. Apps with lower targetSdkVersion values default to opting into legacy storage and would need android:requestLegacyExternalStorage="false" to opt out.

What Happens Next Year?

The documentation still has:

Scoped storage will be required in next year’s major platform release for all apps, independent of target SDK level.

IMHO, this is unwise. Saying that it is required for targetSdkVersion 29 and higher is reasonable. Saying that it is required for all targetSdkVersion values means that lots of legacy apps will crash, as while they hold WRITE_EXTERNAL_STORAGE, they would be ineligible to write to previously-valid locations.

My hope is that Android R will “only” deprecate and ignore developer-supplied android:requestLegacyExternalStorage values, while setting the defaults to be:

  • true for targetSdkVersion 28 and older

  • false for 29 and newer

That ensures the maximum compatibility with legacy apps while still enforcing the new rules for actively-maintained apps.

It is unlikely that Android Q itself will change, though I cannot rule out an Android 10.1 (Q MR1) that messes with this stuff.

So, by August 2020 or thereabouts — whenever Android R ships — you will need to adapt to the new normal.

The idea is that you start adapting now. For some apps, switching to the Storage Access Framework will be easy. For some apps, it will be painful. Do not wait until 2020. Start migrating your apps now to using the alternative approaches. The Storage Access Framework (mostly) works back to Android 4.4, for example, and so many apps will have access to that set of Intent actions.

You can create a dedicated build type or product flavor where you set android:requestLegacyExternalStorage="false" and opt out of the legacy storage support. There, you can see what breaks and start creating plans to fix it.

Hey, Why Am I Seeing Deprecation Warnings?

If your code refers to Environment.getExternalStorageDirectory() or Environment.getExternalStoragePublicDirectory(), you will see that they are deprecated. They still work, but the deprecation warning is yet another nudge to remind you that you need to stop using those.

Once you no longer have the legacy storage model, those directories are unusable, which (presumably) is why they are deprecated.

How Can My Library Know What To Do?

In a library, you do not control whether the app is in normal or legacy mode. If you need different code for those two cases, you can call Environment.isExternalStorageLegacy(), which will return true if the app is in legacy mode, false otherwise.

What Happens When the App is Uninstalled?

Any files that you wrote to external storage in getExternalFilesDir() get removed, as normal.

If you were thinking of switching your Environment.getExternalStorageDirectory() and Environment.getExternalStoragePublicDirectory() code to use getExternalFilesDir(), that will work, but the cost is that your files go away when the app is uninstalled.

For files that are owned by the user and should remain after your app is removed, use the Storage Access Framework or MediaStore.

So, There Is Nothing I Need to Worry About Today?

Well, there may be. Some script-kiddie workarounds to avoid existing limits may cause your app to break on Android Q. Here are two examples:

Inhibiting FileUriExposedException

Back in Android 7.0, Google added logic to StrictMode to see if you have a Uri in your Intent that has the file scheme. If it does, and you use that Intent for something like startActivity(), your app would crash with a FileUriExposedException. The right solution is to use FileProvider or otherwise get a content Uri. However, some developers elected to reconfigure StrictMode to block that check and prevent the exception.

Technically, that hack still works, in that you should not crash with the exception. However, apps that opt out of the legacy filesystem support cannot access your file. So they crash when they try to use your Uri, and your users lose whatever functionality you were trying to offer by starting that third-party activity.

Reading DATA

Some developers, particularly for ACTION_PICK from the MediaStore, would query the MediaStore and read the DATA column to try to get a filesystem path corresponding to the picked content. That has not been reliable in quite some time, but I am sure that some developers are still using it.

Well, in Android Q, the DATA column is blocked, and you will not be able to get the values.

You will need to use the Uri that you get as intended, with a mix of ContentResolver (e.g., openInputStream()) and DocumentFile.fromSingleUri() (e.g., getName()).

Is There Anything Else? This Is Getting Rather Long.

Keep an eye out for future Q beta releases, as while the API is supposed to be stable, there still might be functionality changes.

If I stumble upon any new problems, I’ll write about them. If you encounter scoped storage bugs new to Q Beta 4, ping me on Twitter or reach out via email.