As of Android Q Beta 4, apps that target Android 9 (API level 28)
or lower see no change, by default, to how storage works from previous Android
versions. As you update your existing app to work with scoped storage, you can
use the new
requestLegacyExternalStorage manifest attribute to enable the
new behavior for your app on Android Q devices, even if your app is targeting
Android 9 or lower.
To give users more control over their files and to limit file clutter, Android Q
changes how apps can access files on the device's external storage, such as the
files stored at the path /sdcard. Android Q continues to use the
READ_EXTERNAL_STORAGE
and
WRITE_EXTERNAL_STORAGE
permissions, which correspond to the Storage user-facing runtime permission.
However, apps targeting Android Q by default (as well as apps that opt into the
change) are given a filtered view into external storage. Such
apps can see only their app-specific directory and specific types of media, so
the apps don't need to request any additional user permissions.
This guide describes the files included in the filtered view, as well as how to update your app so that it can continue to share, access, and modify files that are saved on an external storage device. This guide also explains several considerations related to location information in photographs, media access from native code, and use of column names in content queries.
To learn more about changes to external storage in Android Q, see the section that discusses Improvements in creating files on external storage.
Filtered view into external storage
By default, if your app targets Android Q, it has a filtered view of the
files that are on an external storage device. The app can store files intended
for itself under an app-specific directory using
Context.getExternalFilesDir().
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. Your app doesn't need to declare any storage permissions to access these files.
Your app can access files that other apps have created only if both of the following conditions are true:
- Your app has been granted the
READ_EXTERNAL_STORAGEpermission. The files reside in one of the following well-defined media collections:
- Photos, which are stored in
MediaStore.Images. - Videos, which are stored in
MediaStore.Video. - Music files, which are stored in
MediaStore.Audio.
- Photos, which are stored in
In order to access any other file that another app has created, including files in a "downloads" directory, your app must use the Storage Access Framework, which allows the user to select a specific file.
The filtered view also imposes the following media-related data restrictions:
- The Exif metadata within image files is redacted, unless your app has been
granted the
ACCESS_MEDIA_LOCATIONpermission. Learn more in the section about how to access location information in pictures. - The
DATAcolumn is redacted for each file in the media store. - The
MediaStore.Filestable is itself filtered, showing only photos, videos, and audio files. For example, this table no longer shows PDF files.
To access media files in native code, retrieve the file using
MediaStore in your Java-based or
Kotlin-based code, then pass the corresponding file descriptor into your native
code. For more information, see the section on how to access media files from
native code.
Preserve your app's files after uninstall
If an app has a filtered view into external storage and the app is then
uninstalled, all files within the app-specific directory are cleaned up. To
preserve these files after an uninstall, save them to a directory within the
MediaStore.
Opt out of filtered view
Most apps that already follow storage best practices should work with scoped
storage after making minimal changes. Before your app is fully compatible or
tested, you can temporarily opt out of the scoped storage behavior based on your
app's target SDK level or a new manifest attribute called
requestLegacyExternalStorage:
-
Target Android 9 (API level 28) or lower.
-
If you target Android Q, set the value of
requestLegacyExternalStoragetotruein your app's manifest file:<manifest ... > <!-- This attribute is "false" by default on apps targeting Android Q. --> <application android:requestLegacyExternalStorage="true" ... > ... </application> </manifest>
Set up a virtual external storage device
On devices without removable external storage, use the following command to enable a virtual disk for testing purposes:
adb shell sm set-virtual-disk true
Summary of filtered view file access
The following table summarizes how an app that has a filtered view into external storage can access files:
| File location | Permissions needed | Method of accessing (*) | Files removed when app uninstalled? |
|---|---|---|---|
| App-specific directory | None |
getExternalFilesDir() |
Yes |
| Media collections (photos, videos, audio) |
READ_EXTERNAL_STORAGEonly when accessing other apps' files |
MediaStore |
No |
| Downloads (documents and e-books) |
None | Storage Access
Framework (loads system's file picker) |
No |
*You can use the Storage Access Framework to access each of the locations shown in the preceding table without requesting any permissions.
Adapt specific types of usage patterns to the change
This section provides advice for several specific types of media-based apps to adapt to the storage behavior change taking place in apps that target Android Q.
It's a best practice to use the filtered view unless your app needs access to
a file that doesn't reside in either the app-specific directory or the
MediaStore.
Share media files
Some apps allow users to share media files with each other. For example, social media apps give users the ability to share photos and videos with friends.
To access the media files that users want to share, use the
MediaStore API. You can use this
same API to store any files that the user receives through the app, taking
advantage of the improvements introduced in
Android Q.
In cases where you provide a suite of companion apps—such as a messaging
app and a profile app—set up file
sharing using content:// URIs.
We already recommend this workflow as a security best
practice.
Work in documents
Some apps use documents as the unit of storage in which users enter data that they might want to share with peers or import into other documents. Several examples include a user opening a business productivity document or opening a book that's saved as an EPUB file.
In these cases, allow the user to choose the file to open by invoking the
ACTION_OPEN_DOCUMENT
intent, which opens the system's file picker app. To show only the types of
files that your app supports, include the
Intent.EXTRA_MIME_TYPES
extra in your intent.
The ActionOpenDocument
sample on GitHub shows how to use ACTION_OPEN_DOCUMENT to open a file after
getting the user's consent.
Manage groups of files
File management and media creation apps typically manage groups of files in a
directory hierarchy. These apps can invoke the
ACTION_OPEN_DOCUMENT_TREE
intent to allow the user to grant access to an entire directory tree.
Such an app would be able to edit any file in the selected directory, as well
as any of its sub-directories.
Using this interface, users can access files from any installed instance of
DocumentsProvider, which any
locally-backed or cloud-based solution can support.
The ActionOpenDocumentTree
sample on GitHub shows how to use ACTION_OPEN_DOCUMENT_TREE to open a
directory tree after getting the user's consent.
Access and edit media content
This section provides best practices for loading and storing media files in external storage so that your app continues to provide a good user experience in Android Q.
Note: If an app has a filtered view into external storage and requests the Storage runtime permission, the app can view a given file only if the file resides in the app-specific directory or one of the following media collections:
Even with the Storage permission, such an app that accesses the raw file-system view of an external storage device has access only to the app's raw, package-specific path. If an app attempts to open files outside of its package-specific path using a raw file-system view, an error occurs:
- In managed code, a
FileNotFoundExceptionoccurs. - In native code, an
EPERMkernel error occurs.
Access files
Don't load media files using the deprecated DATA columns. Instead, call one of
the following methods from ContentResolver:
- For the thumbnail of a single media file, use
loadThumbnail(), passing in the size of the thumbnail that you want to load. - For a single media file, use
openFileDescriptor(). - For a collection of media files, use
query().
The following code snippet shows how to access media files:
// Load thumbnail of a specific media item.
val mediaThumbnail = resolver.loadThumbnail(item, Size(640, 480), null)
// Open a specific media item.
resolver.openFileDescriptor(item, mode).use { pfd ->
// ...
}
// Find all videos on a given storage device, including pending files.
val collection = MediaStore.Video.Media.getContentUri(volumeName)
val collectionWithPending = MediaStore.setIncludePending(collection)
resolver.query(collectionWithPending, null, null, null).use { c ->
// ...
}
Access from native code
You might encounter situations where your app needs to work with a particular media file in native code, such as a file that another app has shared with your app or a media file from the user's media collection. In these cases, begin your media file discovery in your Java-based or Koltin-based code, then pass the file's associated file descriptor into your native code.
The following code snippet shows how to pass a media object's file descriptor into your app's native code:
Kotlin
val contentUri: Uri =
ContentUris.withAppendedId(
android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
cursor.getLong(BaseColumns._ID))
val fileOpenMode = "r"
val parcelFd = resolver.openFileDescriptor(uri, fileOpenMode)
val fd = parcelFd?.detachFd()
// Pass the integer value "fd" into your native code. Remember to call
// close(2) on the file descriptor when you're done using it.
Java
Uri contentUri = ContentUris.withAppendedId(
android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
cursor.getLong(Integer.parseInt(BaseColumns._ID)));
String fileOpenMode = "r";
ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);
if (parcelFd != null) {
int fd = parcelFd.detachFd();
// Pass the integer value "fd" into your native code. Remember to call
// close(2) on the file descriptor when you're done using it.
}
To learn more about accessing files in native code, see the Files for Miles talk from Android Dev Summit '18, starting at 15:20.
Update other apps' media files
To modify a given media file that another app originally saved to an external
storage device, catch the
RecoverableSecurityException
that the platform throws. You can then request that the user grant your app
write access to that specific item.
Location information in pictures
Some photographs contain location information in their Exif metadata, which allows users to view the place where a photograph was taken. Because this location information is sensitive, Android Q by default hides this information from your app if it has a filtered view into external storage. This restriction to location information is different from the one that applies to camera characteristics.
If your app needs access to a photograph's location information, complete the following steps:
- Add the new
ACCESS_MEDIA_LOCATIONpermission to your app's manifest. - From your
MediaStoreobject, callsetRequireOriginal(), passing in the URI of the photograph.
An example of this process appears in the following code snippet:
Kotlin
// Get location data from the ExifInterface class. val photoUri = MediaStore.setRequireOriginal(photoUri) contentResolver.openInputStream(photoUri).use { stream -> ExifInterface(stream).run { // If lat/long is null, fall back to the coordinates (0, 0). val latLong = ?: doubleArrayOf(0.0, 0.0) } }
Java
Uri photoUri = Uri.withAppendedPath(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
cursor.getString(idColumnIndex));
final double[] latLong;
// Get location data from the ExifInterface class.
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
double[] returnedLatLong = exifInterface.getLatLong();
// If lat/long is null, fall back to the coordinates (0, 0).
latLong = returnedLatLong != null ? returnedLatLong : new double[2];
// Don't reuse the stream associated with the instance of "ExifInterface".
stream.close();
} else {
// Failed to load the stream, so return the coordinates (0, 0).
latLong = new double[2];
}
Column names in content queries
If your app's code uses a column name projection, such as
mime_type AS MimeType, keep in mind that Android Q requires column names
that are defined the MediaStore API.
If your code depends on a library that expects a column name that's undefined in
the Android API, such as MimeType, use
CursorWrapper to dynamically
translate the column name in your app's process.