Dostęp do plików multimedialnych z pamięci współdzielonej

Aby zapewnić użytkownikom lepsze wrażenia, wiele aplikacji pozwala im współtworzyć i otwierać multimedia dostępne w pamięci zewnętrznej. Platforma ta zapewnia zoptymalizowany indeks do kolekcji multimediów nazywany magazynem multimediów, który umożliwia użytkownikom łatwiejsze pobieranie i aktualizowanie tych plików multimedialnych. Nawet po odinstalowaniu aplikacji te pliki pozostają na urządzeniu użytkownika.

Selektor zdjęć

Zamiast korzystać z sklepu z multimediami, selektor zdjęć na Androida umożliwia użytkownikom bezpieczny, wbudowany sposób wybierania plików multimedialnych bez konieczności przyznawania aplikacji dostępu do całej biblioteki multimediów. Ta funkcja jest dostępna tylko na obsługiwanych urządzeniach. Więcej informacji znajdziesz w przewodniku po selektorze zdjęć.

Sklep z multimediami

Aby wejść w interakcję z abstrakcją w magazynie multimediów, użyj obiektu ContentResolver pobranego z kontekstu aplikacji:

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

System automatycznie skanuje wolumin pamięci zewnętrznej i dodaje pliki multimedialne do tych dobrze zdefiniowanych kolekcji:

  • Obrazy,w tym zdjęcia i zrzuty ekranu, które są przechowywane w katalogach DCIM/ i Pictures/. System doda te pliki do tabeli MediaStore.Images.
  • Filmy, które są przechowywane w katalogach DCIM/, Movies/ i Pictures/. System doda te pliki do tabeli MediaStore.Video.
  • Pliki audio, które są przechowywane w katalogach Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/ i Ringtones/. Dodatkowo system rozpoznaje playlisty audio znajdujące się w katalogach Music/ lub Movies/ oraz nagrania głosowe, które znajdują się w katalogu Recordings/. System doda te pliki do tabeli MediaStore.Audio. Katalog Recordings/ nie jest dostępny na Androidzie 11 (poziom interfejsu API 30) i starszych.
  • pobrane pliki przechowywane w katalogu Download/; Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym te pliki są przechowywane w tabeli MediaStore.Downloads. Ta tabela nie jest dostępna na Androidzie 9 (poziom interfejsu API 28) i starszych wersjach.

Magazyn multimediów zawiera też kolekcję o nazwie MediaStore.Files. Jego zawartość zależy od tego, czy aplikacja wykorzystuje ograniczone miejsce na dane dostępne w aplikacjach kierowanych na Androida 10 lub nowszego.

  • Jeśli włączone jest ograniczone miejsce na dane, kolekcja zawiera tylko zdjęcia, filmy i pliki audio utworzone przez Twoją aplikację. Większość deweloperów nie musi używać MediaStore.Files do wyświetlania plików multimedialnych z innych aplikacji, ale jeśli masz do tego jakieś szczególne wymagania, możesz zadeklarować uprawnienie READ_EXTERNAL_STORAGE. Zalecamy jednak używanie interfejsów API MediaStore do otwierania plików, które nie zostały utworzone przez Twoją aplikację.
  • Jeśli miejsce na dane o zakresie jest niedostępne lub nie jest używane, kolekcja wyświetla wszystkie typy plików multimedialnych.

Poproś o niezbędne uprawnienia

Przed wykonywaniem operacji na plikach multimedialnych upewnij się, że aplikacja ma zadeklarowane uprawnienia dostępu do tych plików. Uważaj jednak, by nie zadeklarować uprawnień, których aplikacja nie potrzebuje ani nie używa.

Uprawnienia do przechowywania danych

To, czy aplikacja potrzebuje uprawnień dostępu do pamięci, zależy od tego, czy ma dostęp tylko do własnych plików multimedialnych czy plików utworzonych przez inne aplikacje.

Dostęp do własnych plików multimedialnych

Na urządzeniach z Androidem 10 lub nowszym nie potrzebujesz uprawnień związanych z miejscem na dane, aby otwierać i modyfikować pliki multimedialne, które należą do Twojej aplikacji, w tym pliki w kolekcji MediaStore.Downloads. Jeśli na przykład tworzysz aplikację aparatu, nie musisz prosić o uprawnienia związane z pamięcią, aby uzyskać dostęp do zrobionych zdjęć, ponieważ Twoja aplikacja jest właścicielem obrazów zapisywanych w sklepie multimedialnym.

Dostęp do plików multimedialnych innych aplikacji

Aby uzyskać dostęp do plików multimedialnych tworzonych przez inne aplikacje, musisz zadeklarować odpowiednie uprawnienia związane z pamięcią. Pliki muszą znajdować się w jednej z tych kolekcji multimediów:

Jeśli plik można wyświetlić z poziomu zapytań MediaStore.Images, MediaStore.Video lub MediaStore.Audio, można go też wyświetlić za pomocą zapytania MediaStore.Files.

Ten fragment kodu pokazuje, jak zadeklarować odpowiednie uprawnienia do przechowywania danych:

<!-- 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" />

Potrzebne są dodatkowe uprawnienia aplikacjom działającym na starszych urządzeniach

Jeśli Twoja aplikacja jest używana na urządzeniu z Androidem 9 lub starszym albo jeśli tymczasowo zrezygnuje z limitu miejsca na dane, musisz poprosić READ_EXTERNAL_STORAGE o uprawnienia dostępu do wszystkich plików multimedialnych. Jeśli chcesz modyfikować pliki multimedialne, musisz też poprosić o uprawnienie WRITE_EXTERNAL_STORAGE.

Platforma dostępu do pamięci masowej wymagana do uzyskania dostępu do plików pobranych z innych aplikacji

Jeśli aplikacja chce uzyskać dostęp do pliku w kolekcji MediaStore.Downloads, która nie została utworzona przez Ciebie, musisz użyć platformy Storage Access Framework. Więcej informacji o korzystaniu z tej platformy znajdziesz w artykule Uzyskiwanie dostępu do dokumentów i innych plików z pamięci współdzielonej.

Dostęp do lokalizacji multimediów

Jeśli Twoja aplikacja jest kierowana na Androida 10 (poziom interfejsu API 29) lub nowszego i musi pobierać niezredagowane metadane EXIF ze zdjęć, w pliku manifestu aplikacji musisz zadeklarować uprawnienia ACCESS_MEDIA_LOCATION, a następnie poprosić o nie w czasie działania.

Sprawdź dostępność aktualizacji magazynu multimediów

Aby uzyskać bardziej niezawodny dostęp do plików multimedialnych, zwłaszcza jeśli aplikacja zapisuje w pamięci podręcznej identyfikatory URI lub dane z magazynu multimediów, sprawdź, czy wersja magazynu multimediów uległa zmianie w porównaniu do ostatniej synchronizacji danych multimediów. Aby sprawdzić dostępność aktualizacji, wywołaj getVersion(). Zwrócona wersja to unikalny ciąg znaków, który zmienia się zawsze, gdy zawartość magazynu multimediów zmienia się znacząco. Jeśli zwrócona wersja różni się od ostatnio zsynchronizowanej wersji, przeskanuj ją i zsynchronizuj jeszcze raz pamięć podręczną multimediów.

Przeprowadź ten proces podczas uruchamiania procesu aplikacji. Nie musisz sprawdzać wersji za każdym razem, gdy wysyłasz zapytanie do magazynu multimediów.

Nie zakładaj żadnych szczegółów implementacji dotyczących numeru wersji.

Tworzenie zapytania do kolekcji multimediów

Aby znaleźć multimedia, które spełniają określony zestaw warunków, np. czas trwania wynoszący co najmniej 5 minut, użyj instrukcji wyboru przypominającej SQL, podobnej do widocznej w tym fragmencie kodu:

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

Wykonując takie zapytanie w aplikacji, pamiętaj o tych kwestiach:

  • Wywołaj metodę query() w wątku instancji roboczej.
  • Indeksy kolumn należy zapisać w pamięci podręcznej, aby nie trzeba było wywoływać metody getColumnIndexOrThrow() za każdym razem, gdy przetwarzasz wiersz z wyniku zapytania.
  • Dołącz identyfikator do identyfikatora URI treści, jak pokazano w tym przykładzie.
  • Urządzenia z Androidem 10 lub nowszym wymagają nazw kolumn zdefiniowanych w interfejsie API MediaStore. Jeśli biblioteka zależna w aplikacji oczekuje nazwy kolumny, która nie jest zdefiniowana w interfejsie API, np. "MimeType", użyj funkcji CursorWrapper, aby dynamicznie przetłumaczyć nazwę kolumny w procesie aplikacji.

Wczytywanie miniatur plików

Jeśli Twoja aplikacja wyświetla wiele plików multimedialnych i prosi użytkownika o wybranie jednego z tych plików, lepiej wczytać wersje podglądu (miniatury) plików, a nie same pliki.

Aby wczytać miniaturę danego pliku multimedialnego, użyj parametru loadThumbnail() i podaj rozmiar miniatury, którą chcesz wczytać, zgodnie z tym fragmentem kodu:

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

Otwieranie pliku multimedialnego

Sposób otwierania pliku multimedialnego zależy od tego, czy treści multimedialne najlepiej przedstawić jako deskryptor pliku, strumień pliku czy bezpośrednią ścieżkę pliku.

Deskryptor pliku

Aby otworzyć plik multimedialny za pomocą deskryptora pliku, użyj logiki podobnej do pokazanej w tym fragmencie kodu:

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

Strumień plików

Aby otworzyć plik multimedialny za pomocą strumienia plików, użyj logiki podobnej do pokazanej w tym fragmencie kodu:

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

Bezpośrednie ścieżki do plików

Aby zapewnić płynniejsze działanie aplikacji z bibliotekami multimediów innych firm, Android 11 (poziom API 30) i nowszy umożliwia korzystanie z interfejsów API innych niż MediaStore w celu uzyskiwania dostępu do plików multimedialnych z pamięci współdzielonej. Zamiast tego możesz uzyskać bezpośredni dostęp do plików multimedialnych, używając jednego z tych interfejsów API:

  • Interfejs API File
  • Biblioteki natywne, np. fopen()

Jeśli nie masz uprawnień związanych z miejscem na dane, możesz uzyskać dostęp do plików w katalogu aplikacji oraz do plików multimedialnych przypisanych do Twojej aplikacji za pomocą interfejsu API File.

Jeśli aplikacja próbuje uzyskać dostęp do pliku za pomocą interfejsu API File, ale nie ma wymaganych uprawnień, wystąpi FileNotFoundException.

Aby uzyskać dostęp do innych plików w pamięci współdzielonej na urządzeniu z Androidem 10 (poziom interfejsu API 29), zalecamy tymczasowe zrezygnowanie z ograniczonego miejsca na dane przez ustawienie opcji requestLegacyExternalStorage na true w pliku manifestu aplikacji. Aby uzyskać dostęp do plików multimedialnych za pomocą natywnych metod dotyczących plików na Androidzie 10, musisz też poprosić o uprawnienie READ_EXTERNAL_STORAGE.

Na co zwrócić uwagę podczas uzyskiwania dostępu do treści multimedialnych

Podczas uzyskiwania dostępu do treści multimedialnych pamiętaj o uwagach omówionych w kolejnych sekcjach.

Dane w pamięci podręcznej

Jeśli aplikacja zapisuje w pamięci podręcznej identyfikatory URI lub dane z magazynu multimediów, okresowo sprawdzaj dostępność aktualizacji magazynu multimediów. Dzięki temu dane przechowywane w pamięci podręcznej po stronie aplikacji są synchronizowane z danymi dostawcy po stronie systemu.

Wydajność

Gdy wykonujesz sekwencyjny odczyt plików multimedialnych przy użyciu bezpośrednich ścieżek do plików, wydajność jest porównywalna do wydajności interfejsu API MediaStore.

Jednak gdy wykonujesz losowe odczyty i zapisy plików multimedialnych przy użyciu bezpośrednich ścieżek, ten proces może być nawet 2 razy wolniejszy. W takich sytuacjach zalecamy używanie interfejsu API MediaStore.

Kolumna DATA

Otwierając istniejący plik multimedialny, w logice możesz użyć wartości kolumny DATA. Wynika to z faktu, że ta wartość ma prawidłową ścieżkę pliku. Nie zakładaj jednak, że plik jest zawsze dostępny. Trzeba być przygotowanym na obsługę wszelkich błędów wejścia-wyjścia opartych na plikach.

Przy tworzeniu lub aktualizowaniu pliku multimedialnego nie używaj wartości z kolumny DATA. Zamiast nich użyj wartości kolumn DISPLAY_NAME i RELATIVE_PATH.

Woluminy miejsca na dane

Aplikacje kierowane na Androida 10 lub nowszego mogą korzystać z unikalnej nazwy przypisanej przez system do każdego woluminu pamięci zewnętrznej. Taki system nazewnictwa pomaga wydajnie porządkować i indeksować treści oraz daje Ci kontrolę nad tym, gdzie są przechowywane nowe pliki multimedialne.

Szczególnie warto pamiętać o tych tomach:

  • VOLUME_EXTERNAL pozwala wyświetlić wszystkie woluminy pamięci współdzielonej na urządzeniu. Możesz odczytać zawartość tego woluminu syntetycznego, ale nie możesz jej modyfikować.
  • VOLUME_EXTERNAL_PRIMARY reprezentuje główną pamięć współdzieloną na urządzeniu. Możesz czytać i modyfikować zawartość tego tomu.

Inne tomy możesz znaleźć, dzwoniąc pod numer 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();

Miejsce, w którym zarejestrowano multimedia

Niektóre zdjęcia i filmy zawierają w metadanych informacje o lokalizacji, która wskazuje miejsce, w którym zrobiono zdjęcie lub nagrano film.

Sposób uzyskiwania dostępu do informacji o lokalizacji w aplikacji zależy od tego, czy potrzebujesz dostępu do tych informacji na potrzeby zdjęcia czy filmu.

Fotografie

Jeśli aplikacja korzysta z ograniczonego miejsca na dane, system domyślnie ukrywa informacje o lokalizacji. Aby uzyskać dostęp do tych informacji:

  1. Poproś o uprawnienie ACCESS_MEDIA_LOCATION w pliku manifestu aplikacji.
  2. Aby uzyskać dokładne bajty zdjęcia z obiektu MediaStore, wywołaj setRequireOriginal() i podaj identyfikator URI zdjęcia, tak jak w tym fragmencie kodu:

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

Filmy

Aby uzyskać dostęp do informacji o lokalizacji w metadanych filmu, użyj klasy MediaMetadataRetriever, jak pokazano w tym fragmencie kodu. Aby korzystać z tej klasy, aplikacja nie musi prosić o żadne dodatkowe uprawnienia.

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

Udostępnianie

Niektóre aplikacje umożliwiają użytkownikom udostępnianie sobie plików multimedialnych. Na przykład aplikacje mediów społecznościowych umożliwiają użytkownikom udostępnianie zdjęć i filmów znajomym.

Aby udostępniać pliki multimedialne, użyj identyfikatora URI content:// zgodnie z zaleceniami w przewodniku tworzenia dostawcy treści.

Atrybucja plików multimedialnych w aplikacji

Gdy ograniczone miejsce na dane jest włączone w przypadku aplikacji kierowanej na Androida 10 lub nowszego, system przypisuje aplikację do każdego pliku multimedialnego. Wskazuje to pliki, do których aplikacja ma dostęp, jeśli nie prosi o przyznanie uprawnień do pamięci. Każdy plik można przypisać tylko do jednej aplikacji. Jeśli więc Twoja aplikacja utworzy plik multimedialny, który będzie przechowywany w kolekcji zdjęć, filmów lub plików audio, będzie miała dostęp do tego pliku.

Jeśli jednak użytkownik odinstaluje i ponownie zainstaluje Twoją aplikację, musisz poprosić READ_EXTERNAL_STORAGE o dostęp do utworzonych przez nią plików. Prośba o przyznanie uprawnień jest wymagana, ponieważ system traktuje plik jako przypisany do poprzednio zainstalowanej wersji aplikacji, a nie nowo zainstalowanej.

Dodawanie elementu

Aby dodać element multimedialny do istniejącej kolekcji, użyj kodu podobnego do tego. Ten fragment kodu uzyskuje dostęp do głośności VOLUME_EXTERNAL_PRIMARY na urządzeniach z Androidem 10 lub nowszym. Dzieje się tak, ponieważ na tych urządzeniach możesz zmienić zawartość woluminu tylko wtedy, gdy jest to wolumin główny, zgodnie z opisem w sekcji Woluminy pamięci masowej.

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

Przełącz stan oczekiwania na plik multimedialny

Jeśli Twoja aplikacja wykonuje potencjalnie czasochłonne działania (np. zapisuje pliki multimedialne), warto mieć wyłączny dostęp do pliku podczas jego przetwarzania. Na urządzeniach z Androidem 10 lub nowszym aplikacja może uzyskać ten wyłączny dostęp, ustawiając wartość flagi IS_PENDING na 1. Tylko Twoja aplikacja może wyświetlać plik, dopóki aplikacja nie zmieni z powrotem wartości parametru IS_PENDING na 0.

Poniższy fragment kodu jest oparty na poprzednim fragmencie. Ten fragment pokazuje, jak używać flagi IS_PENDING do zapisywania długiego utworu w katalogu odpowiadającym kolekcji 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);

Podpowiedź dotycząca lokalizacji pliku

Gdy Twoja aplikacja przechowuje multimedia na urządzeniu z Androidem 10, są one domyślnie porządkowane na podstawie ich typu. Na przykład domyślnie nowe pliki obrazów są umieszczane w katalogu Environment.DIRECTORY_PICTURES, który odpowiada kolekcji MediaStore.Images.

Jeśli Twoja aplikacja zna konkretną lokalizację, w której mogą być przechowywane pliki (np. album ze zdjęciami Pictures/MyVacationPictures), możesz ustawić MediaColumns.RELATIVE_PATH, aby system informował system o miejscu przechowywania nowo zapisanych plików.

Aktualizowanie elementu

Aby zaktualizować plik multimedialny należący do Twojej aplikacji, użyj kodu podobnego do tego:

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

Jeśli zakres pamięci jest niedostępny lub nie jest włączony, proces pokazany w poprzednim fragmencie kodu działa również w przypadku plików, które nie należą do Twojej aplikacji.

Zaktualizuj w kodzie natywnym

Jeśli chcesz pisać pliki multimedialne przy użyciu bibliotek natywnych, przekaż do kodu natywnego deskryptor powiązanego pliku z kodu w języku Java lub Kotlin.

Ten fragment kodu pokazuje, jak przekazać deskryptor pliku obiektu multimedialnego do natywnego kodu aplikacji:

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

Aktualizowanie plików multimedialnych innych aplikacji

Jeśli aplikacja wykorzystuje ograniczone miejsce na dane, zwykle nie może zaktualizować pliku multimedialnego przesłanego przez inną aplikację do sklepu multimedialnego.

Możesz jednak uzyskać zgodę użytkownika na zmodyfikowanie pliku, przechwytując parametr RecoverableSecurityException generowany przez platformę. Następnie możesz poprosić użytkownika o przyznanie uprawnień do zapisu tego konkretnego elementu w aplikacji, jak pokazano w tym fragmencie kodu:

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

Wykonuj tę procedurę za każdym razem, gdy aplikacja musi zmodyfikować plik multimedialny, którego nie utworzyła.

Jeśli Twoja aplikacja działa na Androidzie 11 lub nowszym, możesz zezwolić użytkownikom na przyznanie jej uprawnień do zapisu grupy plików multimedialnych. Użyj metody createWriteRequest() zgodnie z opisem w sekcji dotyczącej zarządzania grupami plików multimedialnych.

Jeśli Twoja aplikacja ma inny przypadek użycia, który nie jest objęty zakresem miejsca na dane, prześlij prośbę o dodanie funkcji i tymczasowo zrezygnuj z limitu miejsca na dane.

Usuwanie elementów

Aby usunąć w sklepie multimedialnym produkt, którego aplikacja nie potrzebuje już w sklepie, użyj logiki podobnej do pokazanej w tym fragmencie kodu:

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

Jeśli miejsce na dane o zakresie jest niedostępne lub nie jest włączone, możesz użyć poprzedniego fragmentu kodu, aby usunąć pliki należące do innych aplikacji. Jeśli jednak zakres pamięci jest włączony, musisz przechwycić RecoverableSecurityException dla każdego pliku, który aplikacja chce usunąć. Więcej informacji znajdziesz w sekcji dotyczącej aktualizowania elementów multimedialnych.

Jeśli Twoja aplikacja działa na Androidzie 11 lub nowszym, możesz zezwolić użytkownikom na wybranie grupy plików multimedialnych do usunięcia. Użyj metody createTrashRequest() lub createDeleteRequest() zgodnie z opisem w sekcji dotyczącej zarządzania grupami plików multimedialnych.

Jeśli Twoja aplikacja ma inny przypadek użycia, który nie jest objęty zakresem miejsca na dane, prześlij prośbę o dodanie funkcji i tymczasowo zrezygnuj z limitu miejsca na dane.

Wykrywanie aktualizacji plików multimedialnych

W przypadku aplikacji może być konieczne określenie woluminów pamięci zawierających pliki multimedialne dodane lub zmodyfikowane przez aplikacje w porównaniu z poprzednim punktem w czasie. Aby jak najdokładniej wykryć te zmiany, przekaż ilość miejsca na dane do usługi getGeneration(). O ile wersja magazynu multimediów nie ulegnie zmianie, wartość zwrotna tej metody będzie rosnąć monotonicznie w czasie.

Dokładniej mówiąc, getGeneration() działa lepiej niż daty w kolumnach multimediów, np. DATE_ADDED i DATE_MODIFIED. Wynika to z tego, że wartości w tych kolumnach mogą się zmienić, gdy aplikacja wywoła setLastModified() lub gdy użytkownik zmieni zegar systemowy.

Zarządzanie grupami plików multimedialnych

Na Androidzie 11 i nowszych możesz poprosić użytkownika o wybranie grupy plików multimedialnych, a następnie jednorazowa aktualizacja tych plików multimedialnych. Metody te zapewniają większą spójność między urządzeniami, a metody ułatwiają użytkownikom zarządzanie kolekcjami multimediów.

Oto metody, które udostępniają tę funkcję „aktualizacji zbiorczej”:

createWriteRequest()
Poproś użytkownika, aby przyznał Twojej aplikacji uprawnienia do zapisu do określonej grupy plików multimedialnych.
createFavoriteRequest()
Poproś użytkownika o oznaczenie wybranych plików multimedialnych jako ulubionych multimediów na urządzeniu. Każda aplikacja, która ma uprawnienia do odczytu tego pliku, może zobaczyć, że użytkownik oznaczył go jako „Ulubione”.
createTrashRequest()

Poproś użytkownika o umieszczenie określonych plików multimedialnych w koszu urządzenia. Elementy umieszczone w koszu są trwale usuwane po okresie zdefiniowanym przez system.

createDeleteRequest()

Poproś użytkownika o natychmiastowe usunięcie określonych plików multimedialnych bez wcześniejszego umieszczenia ich w koszu.

Po wywołaniu dowolnej z tych metod system tworzy obiekt PendingIntent. Gdy aplikacja wywoła tę intencję, użytkownicy zobaczą okno z prośbą o zgodę na zaktualizowanie lub usunięcie określonych plików multimedialnych.

Tak na przykład wygląda struktura wywołania 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);

oceniać odpowiedź użytkownika, Jeśli użytkownik wyraził zgodę, kontynuuj operację związaną z multimediami. W przeciwnym razie wyjaśnij użytkownikowi, dlaczego aplikacja potrzebuje danego uprawnienia:

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

Tego samego ogólnego wzorca możesz używać w przypadku createFavoriteRequest(), createTrashRequest() i createDeleteRequest().

Uprawnienia do zarządzania multimediami

Użytkownicy mogą ufać określonej aplikacji do zarządzania multimediami, np. częstego edytowania plików multimedialnych. Jeśli Twoja aplikacja jest kierowana na Androida 11 lub nowszego i nie jest domyślną aplikacją galerii na urządzeniu, przy każdej próbie zmodyfikowania lub usunięcia pliku przez aplikację musi wyświetlić się okno potwierdzenia.

Jeśli Twoja aplikacja jest kierowana na Androida 12 (poziom interfejsu API 31) lub nowszego, możesz poprosić użytkowników o przyznanie aplikacji specjalnych uprawnień do zarządzania multimediami. Dzięki temu uprawnieniu aplikacja może wykonywać każdą z tych czynności bez konieczności pytania użytkownika o każdą operację pliku:

Aby to zrobić:

  1. Zadeklaruj uprawnienia MANAGE_MEDIA i READ_EXTERNAL_STORAGE w pliku manifestu aplikacji.

    Aby wywołać funkcję createWriteRequest() bez wyświetlania okna potwierdzenia, zadeklaruj też uprawnienie ACCESS_MEDIA_LOCATION.

  2. Pokaż użytkownikowi interfejs swojej aplikacji, aby wyjaśnić, dlaczego warto przyznać jej dostęp do zarządzania multimediami.

  3. Wywołaj działanie intencji ACTION_REQUEST_MANAGE_MEDIA. Użytkownicy zobaczą ekran Aplikacje do zarządzania multimediami w ustawieniach systemu. Tutaj użytkownicy mogą przyznać specjalne uprawnienia dostępu aplikacji.

Przypadki użycia, które wymagają alternatywy dla sklepu multimedialnego

Jeśli Twoja aplikacja pełni głównie jedną z poniższych ról, rozważ zastosowanie alternatywy dla interfejsów API MediaStore.

Praca z innymi typami plików

Jeśli Twoja aplikacja obsługuje dokumenty i pliki, które nie zawierają wyłącznie treści multimedialnych, np. pliki z rozszerzeniem EPUB lub PDF, użyj działania intencji ACTION_OPEN_DOCUMENT opisanego w przewodniku na temat przechowywania dokumentów i innych plików oraz uzyskiwania do nich dostępu zgodnie z opisem w tym przewodniku.

Udostępnianie plików w aplikacjach towarzyszących

Jeśli udostępniasz pakiet aplikacji towarzyszących, np. aplikację do obsługi wiadomości i aplikację profilu, skonfiguruj udostępnianie plików przy użyciu identyfikatorów URI content://. Zalecamy również ten przepływ pracy jako sprawdzoną metodę dotyczącą bezpieczeństwa.

Dodatkowe materiały

Więcej informacji o przechowywaniu multimediów i uzyskiwaniu do nich dostępu znajdziesz w tych materiałach.

Próbki

Filmy