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
orWRITE_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
fortargetSdkVersion 28
and older -
false
for29
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.