Register now for Android Dev Summit 2019!

Android Q privacy change: Scoped storage

As of Android Q Beta 5, 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:

  1. Your app has been granted the READ_EXTERNAL_STORAGE permission.
  2. The files reside in one of the following well-defined media collections:

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_LOCATION permission. Learn more in the section about how to access location information in pictures.
  • The DATA column is redacted for each file in the media store.
  • The MediaStore.Files table 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 requestLegacyExternalStorage to true in your app's manifest file:

    <manifest ... >
      <!-- This attribute is "false" by default on apps targeting Android Q. -->
      <application android:requestLegacyExternalStorage="true" ... >
        ...
      </application>
    </manifest>
    

If an app is installed with legacy external storage enabled, the app remains in this mode until it's uninstalled. This compatibility behavior applies regardless of whether the device is later upgraded to run Android Q, or if the app is later updated to target Android Q.

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_STORAGE
only 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:

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, as shown in the following code snippet:

Kotlin

try {
    // ...
} catch (rse: RecoverableSecurityException) {
    val requestAccessIntentSender = rse.userAction.actionIntent.intentSender

    // In your code, handle IntentSender.SendIntentException.
    startIntentSenderForResult(requestAccessIntentSender, your-request-code,
            null, 0, 0, 0, null)
}

Java

try {
    // ...
} catch (RecoverableSecurityException rse) {
    IntentSender requestAccessIntentSender = rse.getUserAction()
            .getActionIntent().getIntentSender();

    // In your code, handle IntentSender.SendIntentException.
    startIntentSenderForResult(requestAccessIntentSender, your-request-code,
            null, 0, 0, 0, null);
}

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:

  1. Add the new ACCESS_MEDIA_LOCATION permission to your app's manifest.
  2. From your MediaStore object, call setRequireOriginal(), 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.