Cómo acceder a archivos de contenido multimedia desde el almacenamiento compartido

Para proporcionar una experiencia del usuario más enriquecida, muchas apps permiten que los usuarios contribuyan y accedan al contenido multimedia disponible en un volumen de almacenamiento externo. El framework proporciona un índice optimizado para colecciones de contenido multimedia, llamado tienda de contenido multimedia, que permite a los usuarios recuperar y actualizar esos archivos multimedia con más facilidad. Incluso después de desinstalar la app, los archivos permanecen en el dispositivo del usuario.

Selector de fotos

Como alternativa al uso de la tienda de contenido multimedia, el selector de fotos de Android proporciona una forma integrada y segura para que los usuarios seleccionen archivos multimedia, sin necesidad de otorgar a tu app acceso a toda su biblioteca multimedia. Esta función está disponible solo en dispositivos compatibles. Para obtener más información, consulta la guía del selector de fotos.

Tienda de contenido multimedia

Para interactuar con la abstracción de la tienda de contenido multimedia, usa un objeto ContentResolver que obtengas del contexto de tu app:

Kotlin

val projection = arrayOf(media-database-columns-to-retrieve)
val selection = sql-where-clause-with-placeholder-variables
val selectionArgs = values-of-placeholder-variables
val sortOrder = sql-order-by-clause

applicationContext.contentResolver.query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
)?.use { cursor ->
    while (cursor.moveToNext()) {
        // Use an ID column from the projection to get
        // a URI representing the media item itself.
    }
}

Java

String[] projection = new String[] {
        media-database-columns-to-retrieve
};
String selection = sql-where-clause-with-placeholder-variables;
String[] selectionArgs = new String[] {
        values-of-placeholder-variables
};
String sortOrder = sql-order-by-clause;

Cursor cursor = getApplicationContext().getContentResolver().query(
    MediaStore.media-type.Media.EXTERNAL_CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    sortOrder
);

while (cursor.moveToNext()) {
    // Use an ID column from the projection to get
    // a URI representing the media item itself.
}

El sistema analiza automáticamente un volumen de almacenamiento externo y agrega archivos multimedia a las siguientes colecciones bien definidas:

  • Imágenes, incluidas fotografías y capturas de pantalla, que se almacenan en los directorios DCIM/ y Pictures/. El sistema agrega estos archivos a la tabla MediaStore.Images
  • Videos, que se almacenan en los directorios DCIM/, Movies/ y Pictures/. El sistema agrega estos archivos a la tabla MediaStore.Video
  • Archivos de audio, que se almacenan en los directorios Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/ y Ringtones/. Además, el sistema reconoce playlists de audio que se encuentran en los directorios Music/ o Movies/, además de grabaciones de voz que se encuentran en el directorio Recordings/. El sistema agrega estos archivos a la tabla MediaStore.Audio. El directorio Recordings/ no está disponible en Android 11 (nivel de API 30) ni versiones anteriores
  • Archivos descargados, que se almacenan en el directorio Download/. En los dispositivos que ejecutan Android 10 (nivel 29 de API) y versiones posteriores, estos archivos se almacenan en la tabla MediaStore.Downloads. Esta tabla no está disponible en Android 9 (nivel de API 28) ni en versiones anteriores

La tienda de contenido multimedia también incluye una colección llamada MediaStore.Files. Su contenido depende de si la app usa el almacenamiento específico, que está disponible en apps orientadas a Android 10 o versiones posteriores:

  • Si el almacenamiento específico está habilitado, la colección solo muestra las fotos, los videos y los archivos de audio que creó tu app. La mayoría de los desarrolladores no necesitan usar MediaStore.Files para ver los archivos multimedia de otras apps, pero si tienes un requisito específico para hacerlo, puedes declarar el permiso READ_EXTERNAL_STORAGE. Sin embargo, te recomendamos que uses las APIs de MediaStore para abrir archivos que tu app no haya creado.
  • Si el almacenamiento específico no está disponible o no se usa, la colección muestra todos los tipos de archivos multimedia.

Cómo solicitar los permisos necesarios

Antes de realizar operaciones en archivos multimedia, asegúrate de que la app declare los permisos necesarios para acceder a esos archivos. Sin embargo, ten cuidado de no declarar permisos que tu app no necesita ni usa.

Permisos de almacenamiento

Los permisos para acceder al almacenamiento serán necesarios según si tu app solo accede a sus propios archivos multimedia o a archivos creados por otras apps.

Cómo acceder a tus propios archivos multimedia

En dispositivos con Android 10 o versiones posteriores, no necesitas ningún permiso relacionado con el almacenamiento para acceder y modificar archivos multimedia que pertenezcan a tu app, incluidos los archivos de la colección MediaStore.Downloads. Si estás desarrollando una app de cámara, por ejemplo, no necesitas solicitar permisos relacionados con el almacenamiento para acceder a las fotos que toma, ya que la app es propietaria de las imágenes que escribes en la tienda de contenido multimedia.

Cómo acceder a los archivos multimedia de otras apps

Para acceder a los archivos multimedia que crearon otras apps, debes declarar los correspondientes permisos relacionados con el almacenamiento, y los archivos deben estar en una de las siguientes colecciones de contenido multimedia:

Siempre que un archivo se pueda ver desde las consultas MediaStore.Images, MediaStore.Video o MediaStore.Audio, también se podrá ver con la consulta MediaStore.Files.

En el siguiente fragmento de código, se muestra la declaración de los correspondientes permisos de almacenamiento:

<!-- Required only if your app needs to access images or photos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<!-- Required only if your app needs to access videos
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- Required only if your app needs to access audio files
     that other apps created. -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="29" />

Se necesitan permisos adicionales para las apps que se ejecutan en dispositivos heredados

Si tu app se usa en un dispositivo que ejecuta Android 9 o versiones anteriores, o si se inhabilitó temporalmente el almacenamiento específico, debes solicitar el permiso READ_EXTERNAL_STORAGE para acceder a los archivos multimedia. Si deseas modificar archivos multimedia, también debes solicitar el permiso WRITE_EXTERNAL_STORAGE.

El framework de acceso al almacenamiento es obligatorio para acceder a las descargas de otras apps

Si tu app quiere acceder a un archivo incluido en la colección MediaStore.Downloads que no creó, debes usar el framework de acceso al almacenamiento. Para obtener más información sobre cómo usar este framework, consulta Cómo acceder a documentos y otros archivos desde el almacenamiento compartido.

Permiso de ubicación de contenido multimedia

Si tu app está orientada a Android 10 (nivel de API 29) o versiones posteriores, para que recupere metadatos de EXIF sin ocultar de las fotos, debes declarar el permiso ACCESS_MEDIA_LOCATION en el manifiesto de tu app y, luego, solicitar este permiso durante el tiempo de ejecución.

Cómo buscar actualizaciones de la tienda de contenido multimedia

Para acceder a los archivos multimedia de manera más confiable, en especial si tu app almacena en caché URIs o datos de la tienda multimedia, comprueba si cambió la versión de la tienda cuando se sincronizaron los datos por última vez. Para realizar esta búsqueda de actualizaciones, llama a getVersion(). La versión que se devuelve es una cadena única que cambia cada vez que la tienda de contenido multimedia cambia sustancialmente. Si la versión que se muestra es diferente de la última versión sincronizada, vuelve a analizar y sincronizar la caché de contenido multimedia de tu app.

Completa esta verificación en el momento de inicio del proceso de la app. No es necesario verificar la versión cada vez que consultas la tienda de contenido multimedia.

No supongas ningún detalle de la implementación acerca del número de la versión.

Cómo buscar una colección de contenido multimedia

Para encontrar contenido multimedia que cumpla con un conjunto determinado de condiciones, como una duración de 5 minutos o más, usa una sentencia de selección similar a SQL, parecida a la que se muestra en el siguiente fragmento de código:

Kotlin

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
data class Video(val uri: Uri,
    val name: String,
    val duration: Int,
    val size: Int
)
val videoList = mutableListOf<Video>()

val collection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Video.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL
        )
    } else {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    }

val projection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    // Cache column indices.
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
    val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
    val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        val id = cursor.getLong(idColumn)
        val name = cursor.getString(nameColumn)
        val duration = cursor.getInt(durationColumn)
        val size = cursor.getInt(sizeColumn)

        val contentUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList += Video(contentUri, name, duration, size)
    }
}

Java

// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didn't create.

// Container for information about each video.
class Video {
    private final Uri uri;
    private final String name;
    private final int duration;
    private final int size;

    public Video(Uri uri, String name, int duration, int size) {
        this.uri = uri;
        this.name = name;
        this.duration = duration;
        this.size = size;
    }
}
List<Video> videoList = new ArrayList<Video>();

Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
    collection = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
}

String[] projection = new String[] {
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
};
String selection = MediaStore.Video.Media.DURATION +
        " >= ?";
String[] selectionArgs = new String[] {
    String.valueOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
};
String sortOrder = MediaStore.Video.Media.DISPLAY_NAME + " ASC";

try (Cursor cursor = getApplicationContext().getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    // Cache column indices.
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    int nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME);
    int durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION);
    int sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE);

    while (cursor.moveToNext()) {
        // Get values of columns for a given video.
        long id = cursor.getLong(idColumn);
        String name = cursor.getString(nameColumn);
        int duration = cursor.getInt(durationColumn);
        int size = cursor.getInt(sizeColumn);

        Uri contentUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);

        // Stores column values and the contentUri in a local object
        // that represents the media file.
        videoList.add(new Video(contentUri, name, duration, size));
    }
}

Cuando realices una consulta de este tipo en tu app, ten en cuenta lo siguiente:

  • Llama al método query() en un subproceso de trabajo.
  • Almacena los índices de columna en caché para no tener que llamar a getColumnIndexOrThrow() cada vez que proceses una fila desde el resultado de la consulta.
  • Agrega el ID al URI de contenido, como se muestra en este ejemplo.
  • Los dispositivos con Android 10 y versiones posteriores requieren nombres de columna definidos en la API de MediaStore. Si una biblioteca dependiente de la app espera un nombre de columna que no está definido en la API, como "MimeType", usa CursorWrapper para traducir dinámicamente el nombre de la columna en el proceso de la app.

Cómo cargar miniaturas de archivos

Si la app muestra varios archivos multimedia y solicita que el usuario elija uno de estos, es más eficiente cargar versiones preliminares (o miniaturas) de los archivos, en lugar de los archivos en sí.

Para cargar la miniatura de un archivo multimedia determinado, usa loadThumbnail() y pasa el tamaño de la miniatura que desees cargar, como se muestra en el siguiente fragmento de código:

Kotlin

// Load thumbnail of a specific media item.
val thumbnail: Bitmap =
        applicationContext.contentResolver.loadThumbnail(
        content-uri, Size(640, 480), null)

Java

// Load thumbnail of a specific media item.
Bitmap thumbnail =
        getApplicationContext().getContentResolver().loadThumbnail(
        content-uri, new Size(640, 480), null);

Cómo abrir un archivo multimedia

La lógica específica que usas para abrir un archivo multimedia depende de si el contenido multimedia se representa mejor como descriptor de archivos, transmisión de archivos o ruta de acceso a archivos directa:

Descriptor de archivos

Para abrir un archivo multimedia con un descriptor de archivos, usa una lógica similar a la que se muestra en el siguiente fragmento de código:

Kotlin

// Open a specific media item using ParcelFileDescriptor.
val resolver = applicationContext.contentResolver

// "rw" for read-and-write.
// "rwt" for truncating or overwriting existing file contents.
val readOnlyMode = "r"
resolver.openFileDescriptor(content-uri, readOnlyMode).use { pfd ->
    // Perform operations on "pfd".
}

Java

// Open a specific media item using ParcelFileDescriptor.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// "rw" for read-and-write.
// "rwt" for truncating or overwriting existing file contents.
String readOnlyMode = "r";
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(content-uri, readOnlyMode)) {
    // Perform operations on "pfd".
} catch (IOException e) {
    e.printStackTrace();
}

Transmisión de archivos

Para abrir un archivo multimedia con una transmisión de archivos, usa una lógica similar a la que se muestra en el siguiente fragmento de código:

Kotlin

// Open a specific media item using InputStream.
val resolver = applicationContext.contentResolver
resolver.openInputStream(content-uri).use { stream ->
    // Perform operations on "stream".
}

Java

// Open a specific media item using InputStream.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();
try (InputStream stream = resolver.openInputStream(content-uri)) {
    // Perform operations on "stream".
}

Rutas de acceso a archivos directas

Para ayudar a que tu app funcione sin problemas con las bibliotecas multimedia de terceros, Android 11 (nivel de API 30) y las versiones posteriores te permiten usar APIs distintas de la API de MediaStore para acceder a archivos multimedia desde el almacenamiento compartido. En su lugar, puedes acceder a los archivos multimedia directamente con cualquiera de las siguientes APIs:

  • La API de File
  • Bibliotecas nativas, como fopen()

Si no tienes ningún permiso relacionado con el almacenamiento, puedes acceder a los archivos del directorio específico de la app y a los archivos multimedia atribuidos a la app; para ello, usa la API de File.

Si tu app intenta acceder a un archivo con la API de File y no tiene los permisos necesarios, se genera una FileNotFoundException.

Para acceder a otros archivos del almacenamiento compartido en un dispositivo con Android 10 (nivel de API 29), te recomendamos que inhabilites temporalmente el almacenamiento específico estableciendo requestLegacyExternalStorage en true en el archivo de manifiesto de tu app. Para acceder a los archivos multimedia con los métodos de archivos nativos en Android 10, también debes solicitar el permiso READ_EXTERNAL_STORAGE.

Consideraciones para tener en cuenta al acceder a contenido multimedia

Cuando accedas a contenido multimedia, ten en cuenta las consideraciones que se analizan en las siguientes secciones.

Datos almacenados en caché

Si tu app almacena en caché los URIs o los datos de la tienda de contenido multimedia, verifica si hay actualizaciones. Esta verificación permite que los datos almacenados en caché de tu app permanezcan sincronizados con los datos del proveedor del sistema.

Rendimiento

Cuando realizas lecturas secuenciales de archivos multimedia con rutas de archivos directas, el rendimiento es similar al de la API de MediaStore.

Sin embargo, cuando realizas lecturas y escrituras aleatorias de archivos multimedia con rutas de archivos directas, el proceso puede ser hasta dos veces más lento. En estas situaciones, recomendamos usar la API de MediaStore.

Columna DATA

Cuando accedes a un archivo multimedia existente, puedes usar el valor de la columna DATA en tu lógica. Esto se debe a que este valor tiene una ruta de archivo válida. Sin embargo, no debes asumir que el archivo siempre está disponible. Prepárate para controlar cualquier error de E/S basado en archivos que se pueda producir.

Por otro lado, para crear o actualizar un archivo multimedia, no uses el valor de la columna DATA. En su lugar, usa los valores de las columnas DISPLAY_NAME y RELATIVE_PATH.

Volúmenes de almacenamiento

Las apps orientadas a Android 10 o versiones posteriores pueden acceder al nombre único que asigna el sistema a cada volumen de almacenamiento externo. Este sistema de nombres te ayuda a organizar e indexar el contenido de manera eficiente, y te permite controlar dónde se almacenan los nuevos archivos multimedia.

Los siguientes volúmenes son particularmente útiles para tener en cuenta:

  • El volumen VOLUME_EXTERNAL proporciona una vista de todos los volúmenes de almacenamiento compartido en el dispositivo. Puedes leer el contenido de este volumen sintético, pero no puedes modificarlo.
  • El volumen VOLUME_EXTERNAL_PRIMARY representa el volumen de almacenamiento compartido principal en el dispositivo. Puedes leer y modificar el contenido de este volumen.

Para descubrir otros volúmenes, llama a MediaStore.getExternalVolumeNames():

Kotlin

val volumeNames: Set<String> = MediaStore.getExternalVolumeNames(context)
val firstVolumeName = volumeNames.iterator().next()

Java

Set<String> volumeNames = MediaStore.getExternalVolumeNames(context);
String firstVolumeName = volumeNames.iterator().next();

Ubicación donde se capturó el contenido multimedia

Algunas fotos y videos contienen información sobre la ubicación en sus metadatos, que muestra el lugar donde se tomó una fotografía o dónde se grabó un video.

La forma de acceder a esta información de ubicación en tu app depende de si necesitas acceder a la información de ubicación de una foto o un video.

Fotografías

Si tu app usa almacenamiento específico, el sistema oculta la información de ubicación de forma predeterminada. Para acceder a esta información, completa los siguientes pasos:

  1. Solicita el permiso ACCESS_MEDIA_LOCATION en el manifiesto de la app.
  2. Desde el objeto MediaStore, llama a setRequireOriginal() para obtener los bytes exactos de la fotografía y pasa el URI correspondiente, como se muestra en el siguiente fragmento de código:

    Kotlin

    val photoUri: Uri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex)
    )
    
    // Get location data using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    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 = 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 using the Exifinterface library.
    // Exception occurs if ACCESS_MEDIA_LOCATION permission isn't granted.
    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];
    }
    

Videos

Para acceder a la información de ubicación dentro de los metadatos de un video, usa la clase MediaMetadataRetriever, como se muestra en el siguiente fragmento de código. Tu app no necesita solicitar permisos adicionales para usar esta clase.

Kotlin

val retriever = MediaMetadataRetriever()
val context = applicationContext

// Find the videos that are stored on a device by querying the video collection.
val query = ContentResolver.query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)
query?.use { cursor ->
    val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
    while (cursor.moveToNext()) {
        val id = cursor.getLong(idColumn)
        val videoUri: Uri = ContentUris.withAppendedId(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
            id
        )
        extractVideoLocationInfo(videoUri)
    }
}

private fun extractVideoLocationInfo(videoUri: Uri) {
    try {
        retriever.setDataSource(context, videoUri)
    } catch (e: RuntimeException) {
        Log.e(APP_TAG, "Cannot retrieve video file", e)
    }
    // Metadata uses a standardized format.
    val locationMetadata: String? =
            retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
}

Java

MediaMetadataRetriever retriever = new MediaMetadataRetriever();
Context context = getApplicationContext();

// Find the videos that are stored on a device by querying the video collection.
try (Cursor cursor = context.getContentResolver().query(
    collection,
    projection,
    selection,
    selectionArgs,
    sortOrder
)) {
    int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
    while (cursor.moveToNext()) {
        long id = cursor.getLong(idColumn);
        Uri videoUri = ContentUris.withAppendedId(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id);
        extractVideoLocationInfo(videoUri);
    }
}

private void extractVideoLocationInfo(Uri videoUri) {
    try {
        retriever.setDataSource(context, videoUri);
    } catch (RuntimeException e) {
        Log.e(APP_TAG, "Cannot retrieve video file", e);
    }
    // Metadata uses a standardized format.
    String locationMetadata = retriever.extractMetadata(
            MediaMetadataRetriever.METADATA_KEY_LOCATION);
}

Se comparte

Algunas apps permiten que los usuarios compartan archivos multimedia entre sí. Por ejemplo, las apps de redes sociales permiten a los usuarios compartir fotos y videos con amigos.

Para compartir archivos multimedia, usa un URI content://, como se recomienda en la guía para crear un proveedor de contenido.

Atribución de archivos multimedia a apps

Cuando el almacenamiento específico está habilitado para una app orientada a Android 10 o versiones posteriores, el sistema atribuye cada archivo multimedia a una app, lo que determina los archivos a los que puede acceder tu app si no solicitó ningún permiso de almacenamiento. Se puede atribuir cada archivo a una sola app. Por lo tanto, si tu app crea un archivo multimedia que se almacena en la colección de contenido multimedia de fotos, videos o archivos de audio, la app tendrá acceso al archivo.

Sin embargo, si el usuario desinstala y reinstala tu app, debes solicitar el permiso READ_EXTERNAL_STORAGE para acceder a los archivos que la app creó originalmente. Esta solicitud de permiso es obligatoria porque el sistema considera que se atribuye el archivo a la versión instalada previamente de la app, en lugar de a la que se instaló recientemente.

Cómo agregar un elemento

Para agregar un elemento multimedia a una colección existente, usa un código similar al siguiente: Este fragmento de código permite acceder al volumen VOLUME_EXTERNAL_PRIMARY en dispositivos con Android 10 o versiones posteriores. Esto se debe a que, en estos dispositivos, solo puedes modificar el contenido de un volumen si es el principal, como se describe en la sección volúmenes de almacenamiento.

Kotlin

// Add a specific media item.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

// Publish a new song.
val newSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Song.mp3")
}

// Keep a handle to the new song's URI in case you need to modify it
// later.
val myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails)

Java

// Add a specific media item.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

// Publish a new song.
ContentValues newSongDetails = new ContentValues();
newSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Song.mp3");

// Keep a handle to the new song's URI in case you need to modify it
// later.
Uri myFavoriteSongUri = resolver
        .insert(audioCollection, newSongDetails);

Cómo activar o desactivar el estado pendiente para archivos multimedia

Si la app realiza operaciones que pueden tardar mucho tiempo, como escribir en archivos multimedia, resulta práctico contar con acceso exclusivo al archivo mientras se procesa. En dispositivos que ejecutan Android 10 o versiones posteriores, la app puede obtener ese acceso exclusivo si establece el valor de la marca IS_PENDING en 1. Solo tu app podrá ver el archivo hasta que cambie nuevamente el valor de IS_PENDING a 0.

En el siguiente fragmento de código, se muestra el ejemplo en mayor detalle del fragmento de código anterior. Ese fragmento muestra cómo usar la marca IS_PENDING cuando se almacena una canción larga en el directorio correspondiente a la colección MediaStore.Audio:

Kotlin

// Add a media item that other apps don't see until the item is
// fully written to the media store.
val resolver = applicationContext.contentResolver

// Find all audio files on the primary external storage device.
val audioCollection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Audio.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY
        )
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

val songDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Workout Playlist.mp3")
    put(MediaStore.Audio.Media.IS_PENDING, 1)
}

val songContentUri = resolver.insert(audioCollection, songDetails)

// "w" for write.
resolver.openFileDescriptor(songContentUri, "w", null).use { pfd ->
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear()
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0)
resolver.update(songContentUri, songDetails, null, null)

Java

// Add a media item that other apps don't see until the item is
// fully written to the media store.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// Find all audio files on the primary external storage device.
Uri audioCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    audioCollection = MediaStore.Audio.Media
            .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
    audioCollection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

ContentValues songDetails = new ContentValues();
songDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Workout Playlist.mp3");
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 1);

Uri songContentUri = resolver
        .insert(audioCollection, songDetails);

// "w" for write.
try (ParcelFileDescriptor pfd =
        resolver.openFileDescriptor(songContentUri, "w", null)) {
    // Write data into the pending audio file.
}

// Now that you're finished, release the "pending" status and let other apps
// play the audio track.
songDetails.clear();
songDetails.put(MediaStore.Audio.Media.IS_PENDING, 0);
resolver.update(songContentUri, songDetails, null, null);

Cómo optimizar sugerencias para la ubicación del archivo

Cuando tu app almacena contenido multimedia en un dispositivo con Android 10, se organiza ese contenido de forma predeterminada según el tipo. Por ejemplo, de forma predeterminada, los archivos de imagen nuevos se colocan en el directorio Environment.DIRECTORY_PICTURES, que corresponde a la colección MediaStore.Images.

Si la app conoce la ubicación específica donde se deben almacenar los archivos, como un álbum de fotos llamado Pictures/MyVacationPictures, puedes configurar MediaColumns.RELATIVE_PATH para sugerir al sistema dónde almacenar archivos recién escritos.

Cómo actualizar un elemento

Para actualizar un archivo multimedia de propiedad de tu app, usa un código similar al siguiente:

Kotlin

// Updates an existing media item.
val mediaId = // MediaStore.Audio.Media._ID of item to update.
val resolver = applicationContext.contentResolver

// When performing a single item update, prefer using the ID.
val selection = "${MediaStore.Audio.Media._ID} = ?"

// By using selection + args you protect against improper escaping of // values.
val selectionArgs = arrayOf(mediaId.toString())

// Update an existing song.
val updatedSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Favorite Song.mp3")
}

// Use the individual song's URI to represent the collection that's
// updated.
val numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs)

Java

// Updates an existing media item.
long mediaId = // MediaStore.Audio.Media._ID of item to update.
ContentResolver resolver = getApplicationContext()
        .getContentResolver();

// When performing a single item update, prefer using the ID.
String selection = MediaStore.Audio.Media._ID + " = ?";

// By using selection + args you protect against improper escaping of
// values. Here, "song" is an in-memory object that caches the song's
// information.
String[] selectionArgs = new String[] { getId().toString() };

// Update an existing song.
ContentValues updatedSongDetails = new ContentValues();
updatedSongDetails.put(MediaStore.Audio.Media.DISPLAY_NAME,
        "My Favorite Song.mp3");

// Use the individual song's URI to represent the collection that's
// updated.
int numSongsUpdated = resolver.update(
        myFavoriteSongUri,
        updatedSongDetails,
        selection,
        selectionArgs);

Si el almacenamiento específico no está disponible o habilitado, el proceso que se muestra en el fragmento de código anterior también funcionará para los archivos que no son de la app.

Actualización en código nativo

Si necesitas escribir archivos multimedia con bibliotecas nativas, pasa el descriptor de archivo asociado del archivo desde el código basado en Java o Kotlin al código nativo.

En el siguiente fragmento de código, se muestra cómo pasar el descriptor de archivos de un objeto multimedia al código nativo de la app:

Kotlin

val contentUri: Uri = ContentUris.withAppendedId(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(BaseColumns._ID))
val fileOpenMode = "r"
val parcelFd = resolver.openFileDescriptor(contentUri, 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(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        cursor.getLong(Integer.parseInt(BaseColumns._ID)));
String fileOpenMode = "r";
ParcelFileDescriptor parcelFd =
        resolver.openFileDescriptor(contentUri, 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.
}

Cómo actualizar los archivos multimedia de otras apps

Si tu app usa almacenamiento específico, por lo general, no puede actualizar un archivo multimedia que otra app colocó en el almacenamiento multimedia.

Sin embargo, puedes obtener el consentimiento del usuario para modificar el archivo si se captura la RecoverableSecurityException que arroja la plataforma. Luego puedes pedirle al usuario que le permita a tu app realizar operaciones de escritura en ese elemento específico, como se muestra en el siguiente fragmento de código:

Kotlin

// Apply a grayscale filter to the image at the given content URI.
try {
    // "w" for write.
    contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
        setGrayscaleFilter(it)
    }
} catch (securityException: SecurityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val recoverableSecurityException = securityException as?
            RecoverableSecurityException ?:
            throw RuntimeException(securityException.message, securityException)

        val intentSender =
            recoverableSecurityException.userAction.actionIntent.intentSender
        intentSender?.let {
            startIntentSenderForResult(intentSender, image-request-code,
                    null, 0, 0, 0, null)
        }
    } else {
        throw RuntimeException(securityException.message, securityException)
    }
}

Java

try {
    // "w" for write.
    ParcelFileDescriptor imageFd = getContentResolver()
            .openFileDescriptor(image-content-uri, "w");
    setGrayscaleFilter(imageFd);
} catch (SecurityException securityException) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        RecoverableSecurityException recoverableSecurityException;
        if (securityException instanceof RecoverableSecurityException) {
            recoverableSecurityException =
                    (RecoverableSecurityException)securityException;
        } else {
            throw new RuntimeException(
                    securityException.getMessage(), securityException);
        }
        IntentSender intentSender =recoverableSecurityException.getUserAction()
                .getActionIntent().getIntentSender();
        startIntentSenderForResult(intentSender, image-request-code,
                null, 0, 0, 0, null);
    } else {
        throw new RuntimeException(
                securityException.getMessage(), securityException);
    }
}

Completa este proceso cada vez que tu app necesite modificar un archivo multimedia que no creó.

De manera alternativa, si tu app se ejecuta en Android 11 o versiones posteriores, puedes permitir que los usuarios otorguen acceso de escritura a un grupo de archivos multimedia. Usa el método createWriteRequest(), como se describe en la sección sobre cómo administrar grupos de archivos multimedia.

Si tu app tiene otro caso de uso que no se incluye en el almacenamiento específico, envía una solicitud de función y, luego, inhabilita el almacenamiento específico de forma temporal.

Cómo quitar un elemento

Para quitar un elemento que tu app ya no necesita en la tienda de contenido multimedia, usa una lógica similar a la que se muestra en el siguiente fragmento de código:

Kotlin

// Remove a specific media item.
val resolver = applicationContext.contentResolver

// URI of the image to remove.
val imageUri = "..."

// WHERE clause.
val selection = "..."
val selectionArgs = "..."

// Perform the actual removal.
val numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs)

Java

// Remove a specific media item.
ContentResolver resolver = getApplicationContext()
        getContentResolver();

// URI of the image to remove.
Uri imageUri = "...";

// WHERE clause.
String selection = "...";
String[] selectionArgs = "...";

// Perform the actual removal.
int numImagesRemoved = resolver.delete(
        imageUri,
        selection,
        selectionArgs);

Si el almacenamiento específico no está disponible o habilitado, puedes usar el fragmento de código anterior para quitar los archivos que pertenecen a otras apps. Sin embargo, si el almacenamiento específico está habilitado, deberás capturar una RecoverableSecurityException para cada archivo que desea quitar la app, como se describe en la sección sobre cómo actualizar los elementos multimedia.

Si tu app se ejecuta en Android 11 o versiones posteriores, puedes permitir que los usuarios elijan un grupo de archivos multimedia para quitarlos. Usa el método createTrashRequest() o el método createDeleteRequest(), como se describe en la sección sobre cómo administrar grupos de archivos multimedia.

Si tu app tiene otro caso de uso que no se incluye en el almacenamiento específico, envía una solicitud de función y, luego, inhabilita el almacenamiento específico de forma temporal.

Cómo detectar actualizaciones de archivos multimedia

Es posible que tu app necesite identificar volúmenes de almacenamiento con archivos multimedia que las apps agregaron o modificaron, en comparación con un momento anterior. Para detectar estos cambios de forma más confiable, pasa el volumen de almacenamiento de interés a getGeneration(). Siempre que la versión de la tienda de contenido multimedia no cambie, el valor que se muestra de este método aumenta de forma monotónica con el tiempo.

En particular, getGeneration() es más estable que las fechas en las columnas de medios, como DATE_ADDED y DATE_MODIFIED. Eso se debe a que esos valores de columna de contenido multimedia pueden cambiar cuando una app llama a setLastModified() o cuando el usuario cambia el reloj del sistema.

Cómo administrar grupos de archivos multimedia

En Android 11 y versiones posteriores, puedes pedirle al usuario que seleccione un grupo de archivos multimedia y, luego, que los actualice en una sola operación. Estos métodos ofrecen una mejor coherencia entre dispositivos y permiten que los usuarios administren sus colecciones de contenido multimedia con mayor facilidad.

Los métodos que proporcionan esta funcionalidad de "actualización por lotes" incluyen los siguientes:

createWriteRequest()
Solicita al usuario que otorgue acceso de escritura a la app para el grupo especificado de archivos multimedia.
createFavoriteRequest()
Solicita al usuario que marque los archivos multimedia especificados como algunos de sus archivos multimedia "favoritos" en el dispositivo. Cualquier app que tenga acceso de lectura a este archivo podrá ver que el usuario marcó el archivo como "favorito".
createTrashRequest()

Solicita al usuario que coloque los archivos multimedia especificados en la papelera del dispositivo. Se borran de forma permanente los elementos de la papelera después de un período definido por el sistema.

createDeleteRequest()

Solicita al usuario que borre de forma permanente e inmediata los archivos multimedia especificados, sin colocarlos antes en la papelera.

Después de llamar a cualquiera de estos métodos, el sistema compila un objeto PendingIntent. Una vez que tu app invoca este intent, los usuarios ven un diálogo que solicita su consentimiento para que tu app actualice o borre los archivos multimedia especificados.

Por ejemplo, aquí se muestra cómo estructurar una llamada a createWriteRequest():

Kotlin

val urisToModify = /* A collection of content URIs to modify. */
val editPendingIntent = MediaStore.createWriteRequest(contentResolver,
        urisToModify)

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.intentSender, EDIT_REQUEST_CODE,
    null, 0, 0, 0)

Java

List<Uri> urisToModify = /* A collection of content URIs to modify. */
PendingIntent editPendingIntent = MediaStore.createWriteRequest(contentResolver,
                  urisToModify);

// Launch a system prompt requesting user permission for the operation.
startIntentSenderForResult(editPendingIntent.getIntentSender(),
    EDIT_REQUEST_CODE, null, 0, 0, 0);

Evalúa la respuesta del usuario. Si el usuario otorgó su consentimiento, continúa con la operación multimedia. De lo contrario, explícale al usuario por qué tu app necesita el permiso:

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int,
                 data: Intent?) {
    ...
    when (requestCode) {
        EDIT_REQUEST_CODE ->
            if (resultCode == Activity.RESULT_OK) {
                /* Edit request granted; proceed. */
            } else {
                /* Edit request not granted; explain to the user. */
            }
    }
}

Java

@Override
protected void onActivityResult(int requestCode, int resultCode,
                   @Nullable Intent data) {
    ...
    if (requestCode == EDIT_REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            /* Edit request granted; proceed. */
        } else {
            /* Edit request not granted; explain to the user. */
        }
    }
}

Puedes usar este mismo patrón general con createFavoriteRequest(), createTrashRequest() y createDeleteRequest().

Permiso para administrar contenido multimedia

Los usuarios pueden confiar en que una app determinada llevará a cabo la administración del contenido multimedia, por ejemplo, realizará, con frecuencia, cambios en los archivos multimedia. Si la app se orienta a Android 11 o versiones posteriores, y no es la galería predeterminada del dispositivo, deberás mostrarle un diálogo de confirmación al usuario cada vez que esta intente modificar o borrar un archivo.

Si tu app se orienta a Android 12 (nivel de API 31) o una versión posterior, puedes solicitar que los usuarios otorguen acceso a tu app al permiso especial de administración de medios. Este permiso permite que tu app realice una de las siguientes acciones sin necesidad de solicitarle al usuario cada operación de archivo:

Para ello, completa los siguientes pasos:

  1. Declara el permiso MANAGE_MEDIA y el permiso READ_EXTERNAL_STORAGE en el archivo de manifiesto de la app.

    Para llamar a createWriteRequest() sin mostrar un diálogo de confirmación, también declara el permiso ACCESS_MEDIA_LOCATION.

  2. En la app, muéstrale al usuario una IU para explicarle por qué es posible que desee otorgarle a la app el acceso a la administración del contenido multimedia.

  3. Invoca la acción de intent ACTION_REQUEST_MANAGE_MEDIA, que lleva a los usuarios a la pantalla Apps de administración de multimedia en la configuración del sistema. En esta pantalla, los usuarios pueden otorgar el acceso especial de apps.

Casos prácticos que requieren una alternativa a la tienda de contenido multimedia

Si tu app realiza principalmente una de las siguientes funciones, procura contar con una alternativa a las APIs de MediaStore.

Trabaja con otros tipos de archivos

Si tu app trabaja con documentos y archivos que no incluyen exclusivamente contenido multimedia, como archivos que usan la extensión EPUB o PDF, utiliza la acción de intent ACTION_OPEN_DOCUMENT, como se describe en la guía para almacenar y acceder a documentos y otros archivos.

Cómo compartir archivos en apps complementarias

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 URIs de content://. Este flujo de trabajo es la práctica recomendada de seguridad.

Recursos adicionales

Para obtener más información sobre cómo almacenar y acceder al contenido multimedia, consulta los siguientes recursos.

Ejemplos

Videos