Cambio en la privacidad de Android Q: almacenamiento específico

A partir de la versión Beta 5 de Android Q, las apps orientadas a Android 9 (nivel de API 28) o versiones anteriores no tendrán modificaciones, de manera predeterminada, en cuanto al funcionamiento del almacenamiento de versiones anteriores de Android. Cuando actualizas tu app existente a fin de que funcione con el almacenamiento específico, puedes usar el nuevo atributo de manifiesto requestLegacyExternalStorage para habilitar el nuevo comportamiento de tu app en dispositivos con Android Q, incluso si esta se orienta a Android 9 o versiones anteriores.

A fin de brindarles a los usuarios mayor control sobre sus archivos y limitar la sobrecarga, Android Q modifica la manera en que las apps acceden a los archivos del almacenamiento externo del dispositivo, como aquellos almacenados en la ruta /sdcard. Android Q seguirá usando los permisos READ_EXTERNAL_STORAGE y WRITE_EXTERNAL_STORAGE, que corresponden al permiso de Almacenamiento de tiempo de ejecución para el usuario. No obstante, las apps que se orientan a Android Q de manera predeterminada (y las que aceptan el cambio) tienen una vista filtrada del almacenamiento externo. Esas apps solo pueden ver su directorio específico de la app y tipos determinados de contenido multimedia, por lo que no necesitan solicitar permisos de usuario adicionales.

En esta guía, se describen los archivos incluidos en la vista filtrada y la forma de actualizar tu app de manera que pueda seguir compartiendo, abriendo y modificando archivos guardados en un dispositivo de almacenamiento externo. Además, se explican varias consideraciones relacionadas con la información de ubicación en fotos, el acceso al contenido multimedia desde el código nativo y el uso de nombres de columnas en consultas de contenido.

Para obtener más información sobre los cambios del almacenamiento externo en Android Q, consulta la sección donde se detallan las mejoras en la creación de archivos en el almacenamiento externo.

Vista filtrada del almacenamiento externo

De manera predeterminada, si tu app se orienta a Android Q, tiene una vista filtrada de los archivos que se encuentran en el dispositivo de almacenamiento externo. La app puede almacenar archivos destinados a sí misma en un directorio específico de la app mediante Context.getExternalFilesDir().

Una app que tiene una vista filtrada siempre tiene acceso de lectura/escritura a los archivos que crea, tanto dentro como fuera de su directorio específico. Tu app no necesita declarar ningún permiso de almacenamiento para acceder a estos archivos.

Tu app puede acceder a los archivos que crearon otras apps solo si se cumplen estas dos condiciones:

  1. Tu app recibió el permiso READ_EXTERNAL_STORAGE.
  2. Los archivos se alojan en una de las siguientes colecciones de contenido multimedia bien definidas:

Para acceder a cualquier otro archivo creado por otra app, incluidos los archivos en un directorio de "descargas", tu app debe usar el marco de trabajo de acceso al almacenamiento, que le permite al usuario seleccionar un archivo específico.

La vista filtrada también impone las siguientes restricciones de datos del contenido multimedia:

  • Se ocultan los metadatos EXIF dentro de los archivos de imagen, a menos que tu app tenga el permiso ACCESS_MEDIA_LOCATION. Obtén más información en la sección sobre cómo acceder a la información de ubicación en imágenes.
  • Se oculta la columna DATA de cada archivo en el almacén de contenido multimedia.
  • La tabla MediaStore.Files está filtrada y muestra solo fotos, videos y archivos de audio. Por ejemplo, esta tabla ya no muestra archivos PDF.

Para acceder a los archivos multimedia en código nativo, recupera el archivo mediante MediaStore en el código basado en Java o Kotlin y, luego, transfiere el descriptor del archivo correspondiente a tu código nativo. Para obtener más información, consulta la sección sobre cómo acceder a los archivos multimedia desde el código nativo.

Conserva los archivos de tus apps luego de la desinstalación

Si una app tiene vista filtrada del almacenamiento externo y luego se desinstala, se borran todos los archivos dentro del directorio específico de esa app. Si quieres conservarlos luego de la desinstalación, guárdalos en un directorio dentro de MediaStore.

Inhabilita la vista filtrada

La mayoría de las apps que ya siguen las prácticas recomendadas de almacenamiento deberían funcionar con el almacenamiento específico después de realizar cambios mínimos. Antes de que tu app sea totalmente compatible o se haya terminado de probar, puedes inhabilitar de forma temporal el comportamiento del almacenamiento específico basado en el nivel del SDK de destino de tu app o en un nuevo atributo de manifiesto llamado requestLegacyExternalStorage:

  • Orienta la app a Android 9 (nivel de API 28) o versiones anteriores.

  • Si la orientas a Android Q, define el valor de requestLegacyExternalStorage en true en el archivo de manifiesto de tu app:

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

Si una app se instala con el almacenamiento externo heredado habilitado, la app permanece en este modo hasta que se la desinstala. Este comportamiento de compatibilidad se aplica independientemente de que, más adelante, el dispositivo se actualice para usar Android Q o de que la app se actualice para orientarse a Android Q.

Configura un dispositivo de almacenamiento externo virtual

En aquellos dispositivos sin almacenamiento externo extraíble, usa el siguiente comando para habilitar un disco virtual con fines de prueba:

    adb shell sm set-virtual-disk true
    

Resumen del acceso a archivos con vista filtrada

En la siguiente tabla, se resume la forma en que una app con vista filtrada del almacenamiento externo puede acceder a los archivos:

Ubicación del archivo Permisos requeridos Método de acceso (*) ¿Se quitan los archivos cuando se desinstala la app?
Directorio específico de la app Ninguno getExternalFilesDir()
Colecciones de contenido multimedia
(fotos, videos y audio)
READ_EXTERNAL_STORAGE
solo cuando
se accede a los archivos de otras apps
MediaStore No
Descargas
(documentos y
libros electrónicos)
Ninguno Marco de trabajo de acceso al almacenamiento
(carga el selector de archivos del sistema)
No

*Puedes usar el marco de trabajo de acceso a almacenamiento para acceder a cada una de las ubicaciones que aparecen en la tabla anterior sin solicitar ningún permiso.

Adapta tipos específicos de patrones de uso al cambio

En esta sección, se proporciona asesoramiento sobre cómo adaptar varios tipos específicos de apps basadas en multimedia al cambio de comportamiento del almacenamiento que se realizó en las apps orientadas a Android Q.

La práctica recomendada es usar la vista filtrada a menos que tu app necesite acceder a un archivo que no se encuentra ni en su directorio específico ni en MediaStore.

Comparte archivos multimedia

Algunas apps permiten el uso compartido de archivos multimedia entre ellas. Por ejemplo, las apps de redes sociales brindan a los usuarios la capacidad de compartir fotos y videos con amigos.

Para acceder a los archivos multimedia que los usuarios quieren compartir, usa la API de MediaStore. También puedes usarla para almacenar cualquier archivo que reciba el usuario mediante la app, con lo cual, aprovechas las mejoras que se agregaron a Android Q.

En los casos en los que proporciones un conjunto de apps complementarias (como una app de mensajería y una app de perfil), deberás configurar el uso compartido de archivos mediante los URI content://. Este flujo de trabajo es la práctica recomendada de seguridad.

Trabaja en documentos

Algunas apps usan documentos como la unidad de almacenamiento en donde los usuarios ingresan datos que quieren compartir con sus pares o importar a otros documentos. Entre algunos ejemplos, se incluye un usuario que abre un documento sobre la productividad del negocio o un libro guardado como archivo *.epub.

En estos casos, permite que el usuario elija qué archivo se abrirá invocando el intent ACTION_OPEN_DOCUMENT, que abrirá la app del selector de archivos del sistema. Para que solo se muestren los tipos de archivos compatibles con tu app, deberás agregar Intent.EXTRA_MIME_TYPES en tu intent.

En el ejemplo de ActionOpenDocument de GitHub, se muestra cómo usar ACTION_OPEN_DOCUMENT para abrir un archivo luego de obtener el consentimiento del usuario.

Administra grupos de archivos

Las apps de administración de archivos y creación de contenido multimedia generalmente administran grupos de archivos en una jerarquía de directorios. Estas apps pueden invocar el intent ACTION_OPEN_DOCUMENT_TREE para permitir que el usuario otorgue acceso a un árbol de directorios completo. Esa app podría editar cualquier archivo en el directorio seleccionado, así como cualquiera de los subdirectorios.

Con esta interfaz, los usuarios pueden acceder a los archivos desde cualquier instancia instalada de DocumentsProvider, que es compatible con todas las soluciones basadas en la nube o guardadas en copias locales.

En el ejemplo de ActionOpenDocumentTree de GitHub, se muestra cómo usar ACTION_OPEN_DOCUMENT_TREE para abrir un árbol de directorios luego de obtener el consentimiento del usuario.

Accede al contenido multimedia y edítalo

En esta sección, se brindan prácticas recomendadas para cargar y almacenar archivos multimedia en un almacenamiento externo a fin de que tu app siga proporcionando una buena experiencia del usuario en Android Q.

Nota: Si una app tiene una vista filtrada del almacenamiento externo y solicita el permiso de Almacenamiento de tiempo de ejecución, dicha app puede ver un archivo determinado solo si se encuentra en el directorio específico de la app o en una de las siguientes colecciones de contenido multimedia:

Incluso con el permiso de Almacenamiento, cualquier app que acceda a la vista del sistema de archivos sin procesar de un dispositivo de almacenamiento externo puede acceder solo a su ruta sin procesar específica del paquete. Si una app intenta abrir archivos fuera de su ruta específica del paquete utilizando una vista del sistema de archivos sin procesar, se producirá un error:

Accede a archivos

No cargues archivos multimedia con las columnas DATA obsoletas. En cambio, llama a uno de los siguientes métodos de ContentResolver:

  • Para la miniatura de un solo archivo multimedia, usa loadThumbnail() y transfiere el tamaño de la miniatura que deseas cargar.
  • Para un solo archivo multimedia, usa openFileDescriptor().
  • Para una colección de archivos multimedia, usa query().

El siguiente fragmento de código ejemplifica cómo acceder a los archivos multimedia:

    // 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 ->
        // ...
    }
    

Accede desde el código nativo

Es posible que en ocasiones tu app necesite trabajar con un archivo multimedia específico en código nativo, como un archivo que compartió otra app con la tuya o un archivo multimedia de la colección del usuario. En estos casos, inicia el descubrimiento de archivos multimedia en tu código basado en Java o Kotlin y, luego, transfiere a tu código nativo el descriptor del archivo asociado.

El siguiente fragmento de código ejemplifica cómo transferir el descriptor de archivo de un objeto multimedia al código nativo de tu app:

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.
    }
    

Para obtener más información sobre cómo acceder a los archivos en código nativo, mira la charla Files for Miles de la Android Dev Summit de 2018 (a partir del minuto 15:20).

Actualiza los archivos multimedia de otras apps

Para modificar un archivo multimedia determinado que otra app guardó originalmente en un dispositivo de almacenamiento externo, detecta la RecoverableSecurityException que muestra la plataforma. Luego, puedes pedirle al usuario que le permita a tu app realizar operaciones de escritura en ese elemento, como se muestra en el siguiente fragmento de código:

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);
    }
    

Información sobre la ubicación en fotos

Algunas fotos contienen información sobre la ubicación en sus metadatos EXIF, lo que permite a los usuarios ver el lugar donde se tomaron. Como la información de ubicación es sensible, Android Q la oculta de tu app de forma predeterminada si esta tiene una vista filtrada del almacenamiento externo. Esta restricción es distinta a la relacionada con las características de la cámara.

Si tu app necesita acceder a esa información, sigue estos pasos:

  1. Agrega el nuevo permiso ACCESS_MEDIA_LOCATION al manifiesto de tu app.
  2. Desde el objeto MediaStore, llama a setRequireOriginal() y especifica el URI de la foto.

El siguiente fragmento de código muestra un ejemplo de este proceso:

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];
    }
    

Nombres de columnas en consultas de contenido

Si el código de tu app usa una proyección de nombre de columna, como mime_type AS MimeType, ten en cuenta que Android Q requiere nombres de columnas que estén definidos en la API de MediaStore.

Si tu código depende de una biblioteca que espera un nombre de columna no definido en la API de Android, como MimeType, usa CursorWrapper para traducir de forma dinámica el nombre de la columna en el proceso de tu app.