Save the date! Android Dev Summit is coming to Sunnyvale, CA on Oct 23-24, 2019.

Android Q privacy change: App-scoped and media-scoped storage

As of Android Q Beta 2, this change has the following properties:

  • Affects your app if you access and share files in external storage
  • Mitigate by using isolated sandbox or media collection directories
  • Make sure to enable scoped storage before testing.

We're interested in hearing your feedback! Take this short survey to let us know how you're using the feature. In particular, tell us about use cases impacted by this feature.

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. Android Q replaces the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions with more fine-grained, media-specific permissions, and apps accessing their own files on an external storage device don't require specific permissions. These changes affect how your app saves and accesses files on external storage.

This guide describes how to update your app so that it can continue to share, access, and update files that are saved on an external storage device, provides compatibility considerations, and explains how to toggle this behavior change.

Isolated storage sandbox for app-private files

For each app, Android Q creates an isolated storage sandbox, which restricts other apps' access to the files that your app stores on an external storage device. A common external storage device is /sdcard.

This designation provides the following benefits:

  • The need for fewer permissions. The files in your app's sandbox are private to your app. Therefore, you no longer need any permissions to access and save your own files within external storage.
  • More robust privacy with respect to other apps on the device. No other app can directly access the files in your app's isolated storage sandbox. This access restriction makes it easier for your app to maintain the privacy of the sandboxed files.

The best place to store files on external storage is in the location returned by Context.getExternalFilesDir(), because this location behaves consistently across all Android versions. When using this method, pass in the media environment corresponding to the type of file that you want to create or open. For example, to access or save app-private images, call Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES).

Shared collections for media files

If your app creates files that belong to the user, and that the user expects to be retained when your app is uninstalled, then save them into one of the common media collections, also known as shared collections. Shared collections include: Photos & Videos, Music, and Downloads.

Permissions for viewing other apps' files

Your app doesn't need to request any permissions in order to create and modify its own files within these shared collections. If your app needs to create and modify files that other apps have created, however, it must first request the appropriate permission:

  • Access to other apps' files in the Photos & Videos shared collection requires the READ_MEDIA_IMAGES or READ_MEDIA_VIDEO permission, depending on the type of file that your app needs to access.

  • Access to other apps' files in the Music shared collection requires the READ_MEDIA_AUDIO permission.

Learn more about how to work with other apps' files.

Access shared collections

After requesting the necessary permissions, your app accesses these collections using the MediaStore API:

  • For the Photos & Videos shared collection, use MediaStore.Images or MediaStore.Video.
  • For the Music shared collection, use MediaStore.Audio.
  • For the Downloads shared collection, use MediaStore.Downloads.

Caution: For apps that are newly installed on Android Q, calls to getExternalStoragePublicDirectory() provide access only to the files that your app has stored in its isolated storage sandbox.

To maintain access to other apps' files, update your app's logic in one of the following ways:

  • Use MediaStore and request the READ_MEDIA_* permission that corresponds to the media collection that you'd like to access.
  • Use the Storage Access Framework, which doesn't require any permissions.

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 in shared collections

By default, when the user uninstalls your app, Android Q cleans up the files that you saved into your sandbox. To preserve these files when your app is uninstalled, use the Storage Access Framework, or save files to a shared collection.

To preserve files in a shared collection, insert a new row into the relevant MediaStore collection, populating its columns in the following way:

  • At a minimum, provide values for the DISPLAY_NAME and MIME_TYPE columns.
  • Optionally, you can influence where files are placed on disk using the PRIMARY_DIRECTORY and SECONDARY_DIRECTORY columns.
  • Leave the DATA column undefined. That way, the platform has the flexibility to preserve the file outside your sandbox.

After the row is inserted, you can use APIs like ContentResolver.openFileDescriptor() to read or write data to the newly-created file.

If the user reinstalls your app later, however, your app doesn't have access to those files unless it performs one of the following:

  • Requests the appropriate permission for the collection.
  • Sends a request to the user from the Storage Access Framework.

This situation is similar to the case where an app tries to access another app's files.

Special considerations for photographs

Android Q adds several enhancements to give users better control on how their photographs are accessed on external storage.

Access 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 redacts the information by default. 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

val latLong = if (BuildCompat.isAtLeastQ()) {
    // When running Android Q, get location data from `ExifInterface`.
    photoUri = MediaStore.setRequireOriginal(photoUri)
    contentResolver.openInputStream(photoUri).use { stream ->
        ExifInterface(stream).run {
            // If lat/long is null, fall back to the coordinates (0, 0).
            latLong ?: doubleArrayOf(0.0, 0.0)
        }
    }
} else {
    // On devices running Android 9 (API level 28) and lower, use the
    // media store columns.
    doubleArrayOf(
        cursor.getFloat(latitudeColumnIndex).toDouble(),
        cursor.getFloat(longitudeColumnIndex).toDouble()
    )
}

Java

Uri photoUri = Uri.withAppendedPath(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        cursor.getString(idColumnIndex));

final double[] latLong;
if (BuildCompat.isAtLeastQ()) {
    // When running Android Q, get location data from `ExifInterface`.
    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 {@code ExifInterface}.
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
} else {
    // On devices running Android 9 (API level 28) and lower, use the
    // media store columns.
    latLong = new double[]{
            cursor.getFloat(latitudeColumnIndex),
            cursor.getFloat(longitudeColumnIndex)
    };
}

If your app is a camera app, it doesn't have direct access to the photographs saved in the Photos & Videos shared collection unless it's the device's default Photo Manager app. To direct users to a gallery app, use the ACTION_REVIEW intent.

Work with other apps' files

This section explains how your app can interact with other apps' files that are stored in shared collections.

Access files created by other apps

To access and read media files that other apps have saved to an external storage device, complete the following steps:

  1. Request the necessary permission, based on the shared collection that contains the file you want to access.
  2. Use a ContentResolver object to find and open the file.

Write to files created by other apps

By saving a file to a shared collection, your app becomes that file's owner. Ordinarily, your app can write to a file in a shared collection only if you're the file owner. However, if your app serves as the user's default app for a specific use case, you can also write to files that other apps own:

  • If your app is the user's default Photo Manager app, you can modify image files that other apps saved to the Photos & Videos shared collection.
  • If your app is the users' default Music app, you can modify audio files that other apps saved to the Music shared collection.

To modify media files that other apps originally saved to an external storage device, use a ContentResolver object to find the file and modify it in-place. When performing the edit/modify operation, catch the RecoverableSecurityException so that you can request that the user grant you write access to that specific item.

Access media files 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.

Access specific files

In some use cases, your app might need to open or create files that it doesn't have permission to access:

  • In a photo-editing app, open a drawing.
  • In a business productivity app, save a text document to a location that the user chooses.

For these situations, use the Storage Access Framework, which allows the user to select a specific file to open, or choose a specific location to save a file.

Companion app file sharing

If you manage a suite of apps that require mutual access to each other's files, use content:// URIs, which we already recommended as a security best practice.

For more information, see the documentation on how to set up file sharing.

Compatibility mode for previously installed apps on upgrading devices

The restrictions on accessing files in external storage apply only to apps that target Android Q, or apps that are newly installed on a device running Android Q.

The system places your app's file access privileges into compatibility mode when each of the following conditions is true:

  • Your app targets Android 9 (API level 28) or lower.
  • Your app is installed on a device that upgrades from Android 9 to Android Q.

When your app is in compatibility mode, the following file access behavior applies:

  • Your app can access all files stored within the MediaStore collections, even files that your app hasn't created.
  • The user-facing Storage permission allows or denies your app access to external storage as a whole, rather than individual shared collections like Photos & Videos or Music.

This compatibility mode remains in effect until your app is first uninstalled.

Identify a specific external storage device

In Android 9 (API level 28) and lower, all files on all storage devices appear under the single "external" volume name. Android Q gives each external storage device a unique volume name. This naming system helps you efficiently organize and index content, and it gives you control over where new content is stored.

To uniquely identify a specific file within external storage, you must use both the volume name and the ID together. For example, a file on the primary storage device would be content://media/external/images/media/12, but the corresponding file on a secondary storage device called FA23-3E92 would be content://media/FA23-3E92/images/media/12.

You can access the files stored on a particular volume by passing this volume name into a specific media collection, such as MediaStore.Images.getContentUri().

Get the list of external storage devices

To get the list of names for all currently-available volumes, call MediaStore.getAllVolumeNames(), as shown in the following code snippet:

Kotlin

val volumeNames: Set<String> = MediaStore.getAllVolumeNames(context)

Java

Set<String> volumeNames = MediaStore.getAllVolumeNames(context);

Set up a virtual external storage device

On devices without removable external storage, use the following command to enable to a virtual disk for testing purposes:

adb shell sm set-virtual-disk true

Test the behavior change

To help you make your app compatible with this new behavior change, the platform has provided several ways for you to tweak several parameters associated with the change.

Toggle the behavior change

In Android Q, this behavior change is enabled by default. To disable the change for testing purposes, execute the following command in a terminal window:

adb shell sm set-isolated-storage off

The device restarts after you run this command. If it doesn't, wait a minute and try to run the command again.

To confirm that you've successfully toggled the behavior change, use the following command:

adb shell getprop sys.isolated_storage_snapshot

Test compatibility mode behavior

When testing your app, you can enable compatibility mode for external file storage access by running the following command in a terminal window:

adb shell cmd appops set your-package-name android:legacy_storage allow && \
adb shell am force-stop your-package-name

To disable compatibility mode, uninstall and reinstall your app on Android Q, or run the following command in a terminal window:

adb shell cmd appops set your-package-name android:legacy_storage default && \
adb shell am force-stop your-package-name

Browse external storage as a file manager

To gain broad access to directories within external storage, as file manager apps might do, use the ACTION_OPEN_DOCUMENT_TREE intent. For an example, see the android-DirectorySelection sample on GitHub.