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

Aby zapewnić użytkownikom lepsze wrażenia, wiele aplikacji umożliwia współtworzenie treści i korzystanie z multimediów zapisanych w pamięci zewnętrznej. Platforma zapewnia zoptymalizowany indeks kolekcji multimediów (tzw. sklep z multimediami), co umożliwia użytkownikom łatwiejsze pobieranie i aktualizowanie tych plików multimedialnych. Nawet po odinstalowaniu aplikacji pliki te pozostają na urządzeniu użytkownika.

Selektor zdjęć

Zamiast korzystać ze sklepu z multimediami, możesz użyć selektora zdjęć na Androidzie, który zapewnia bezpieczny, wbudowany sposób użytkownikom 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 selektora zdjęć.

Sklep z multimediami

Aby wejść w interakcję z abstrakcją sklepu multimediów, użyj obiektu ContentResolver pobieranego 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 fotografie i zrzuty ekranu, które są przechowywane w katalogach DCIM/ i Pictures/. System doda te pliki do tabeli MediaStore.Images.
  • Filmy przechowywane w katalogach DCIM/, Movies/ i Pictures/. System doda te pliki do tabeli MediaStore.Video.
  • Pliki audio, które są przechowywane w katalogu Alarms/, Audiobooks/, Music/, Notifications/, Podcasts/ i Ringtones/. Poza tym system rozpoznaje playlisty audio znajdujące się w katalogach Music/ lub Movies/, a także nagrania głosowe znajdujące się w katalogu Recordings/. System doda te pliki do tabeli MediaStore.Audio. Katalog Recordings/ nie jest dostępny na urządzeniach z Androidem 11 (poziom interfejsu API 30) i starszymi wersjami.
  • Pobrane pliki – są przechowywane w katalogu Download/. Na urządzeniach z Androidem 10 (poziom interfejsu API 29) lub nowszym pliki te są przechowywane w tabeli MediaStore.Downloads. Ta tabela jest niedostępna na urządzeniach z Androidem 9 (poziom interfejsu API 28) i starszymi wersjami

Sklep z multimediami zawiera też kolekcję o nazwie MediaStore.Files. Zależy to od tego, czy aplikacja wykorzystuje ograniczone miejsce na dane, które jest dostępne w aplikacjach na Androida 10 lub nowszego.

  • Jeśli pamięć o zakresie jest włączona, w kolekcji będą widoczne 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 musisz to zrobić, 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 aplikację.
  • Jeśli pamięć o zakresie jest niedostępna lub nie jest używana, w kolekcji wyświetlają się wszystkie typy plików multimedialnych.

Poproś o niezbędne uprawnienia

Zanim wykonasz operacje na plikach multimedialnych, upewnij się, że aplikacja zadeklarowała wymagane uprawnienia dostępu do tych plików. Pamiętaj jednak, aby nie zadeklarować uprawnień, których Twoja 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 ona dostęp tylko do własnych plików multimedialnych czy też plików utworzonych przez inne aplikacje.

Dostęp do własnych plików multimedialnych

Na urządzeniach z Androidem 10 lub nowszym uprawnienia związane z miejscem na dane nie są wymagane, aby uzyskiwać dostęp do plików multimedialnych należących do Twojej aplikacji, w tym plików znajdujących się w kolekcji MediaStore.Downloads, oraz modyfikować ich pliki multimedialne. Jeśli np. tworzysz aplikację aparatu, nie musisz prosić o uprawnienia dostępu do zrobionych zdjęć, ponieważ to do Twojej aplikacji należą obrazy, które zapisujesz w sklepie z multimediami.

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 przechowywaniem danych, a pliki muszą się znajdować w jednej z tych kolekcji multimediów:

Jeśli plik można wyświetlić w zapytaniach MediaStore.Images, MediaStore.Video lub MediaStore.Audio, można go też wyświetlić przy użyciu 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" />

Dodatkowe uprawnienia wymagane w przypadku aplikacji działających na starszych urządzeniach

Jeśli Twoja aplikacja jest używana na urządzeniu z Androidem 9 lub starszym albo w przypadku aplikacji tymczasowo wyłączyliśmy jej ograniczone miejsce na dane, musisz poprosić o uprawnienie READ_EXTERNAL_STORAGE na dostęp do dowolnego pliku multimedialnego. Jeśli chcesz zmodyfikować pliki multimedialne, musisz też poprosić o uprawnienie WRITE_EXTERNAL_STORAGE.

Platforma Storage Access Framework jest wymagana do uzyskania dostępu do pobranych plików innych aplikacji

Jeśli aplikacja chce uzyskać dostęp do pliku w kolekcji MediaStore.Downloads, który nie został utworzony przez aplikację, musisz użyć platformy Storage Access Framework. Więcej informacji o korzystaniu z tej platformy znajdziesz w artykule na temat uzyskiwania 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 w wersji 10 (poziom interfejsu API 29) lub nowszej i wymaga pobierania niezmodyfikowanych metadanych EXIF ze zdjęć, zadeklaruj w pliku manifestu aplikacji uprawnienie ACCESS_MEDIA_LOCATION, a potem poproś o to uprawnienie w czasie działania.

Sprawdź, czy są dostępne aktualizacje w sklepie z multimediami

Aby uzyskać bardziej niezawodny dostęp do plików multimedialnych, zwłaszcza jeśli aplikacja przechowuje w pamięci podręcznej identyfikatory URI lub dane ze magazynu multimediów, sprawdź, czy wersja magazynu multimediów zmieniła się w porównaniu z datą ostatniej synchronizacji. Aby sprawdzić dostępność aktualizacji, wywołaj metodę getVersion(). Zwracana wersja to unikalny ciąg znaków, który zmienia się przy każdej istotnej zmianie magazynu multimediów. Jeśli zwrócona wersja różni się od ostatniej zsynchronizowanej wersji, ponownie przeskanuj i zsynchronizuj pamięć podręczną multimediów aplikacji.

Sprawdź to 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, na przykład czas trwania co najmniej 5 minut, użyj instrukcji wyboru podobnej do instrukcji SQL podobnej do przedstawionej 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));
    }
}

Podczas wykonywania takiego zapytania w aplikacji pamiętaj o tych kwestiach:

  • Wywołaj metodę query() w wątku roboczym.
  • Zapisuj indeksy kolumn w pamięci podręcznej, aby nie trzeba było wywoływać funkcji getColumnIndexOrThrow() przy każdym przetwarzaniu wiersza z wyniku zapytania.
  • Dołącz identyfikator do identyfikatora URI treści, tak jak w tym przykładzie.
  • Urządzenia z Androidem 10 lub nowszym wymagają nazw kolumn zdefiniowanych w interfejsie MediaStore API. Jeśli biblioteka zależna w Twojej aplikacji oczekuje nazwy kolumny, która jest niezdefiniowana w interfejsie API, np. "MimeType", użyj CursorWrapper, aby dynamicznie przetłumaczyć nazwę kolumny w procesie aplikacji.

Wczytaj miniatury plików

Jeśli aplikacja wyświetla wiele plików multimedialnych i prosi użytkownika o wybranie jednego z tych plików, skuteczniejszym rozwiązaniem jest wczytanie wersji podglądu lub miniatur plików zamiast samych plików.

Aby wczytać miniaturę danego pliku multimedialnego, użyj parametru loadThumbnail() i podaj rozmiar miniatury, którą chcesz wczytać, tak jak w tym fragmencie 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

Konkretna logika, której używasz do otwierania pliku multimedialnego, zależy od tego, czy jego zawartość najlepiej przedstawić jako deskryptor pliku, strumień pliku czy bezpośrednia ścieżka do pliku.

Deskryptor pliku

Aby otworzyć plik multimedialny za pomocą deskryptora pliku, postępuj zgodnie z metodą podobną do tej przedstawionej 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, postępuj zgodnie z metodą podobną do tej przedstawionej 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 interfejsu API 30) i nowszy umożliwiają dostęp do plików multimedialnych z pamięci współdzielonej za pomocą interfejsów API innych niż MediaStore. Możesz uzyskiwać dostęp do plików multimedialnych bezpośrednio za pomocą jednego z tych interfejsów API:

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

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

Jeśli aplikacja próbuje uzyskać dostęp do pliku za pomocą interfejsu API File i nie ma wymaganych uprawnień, pojawia się 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 wyłączenie ograniczonego miejsca na dane przez ustawienie wartości requestLegacyExternalStorage na true w pliku manifestu aplikacji. Aby uzyskać dostęp do plików multimedialnych na Androidzie 10 za pomocą natywnych metod, musisz też poprosić o uprawnienie READ_EXTERNAL_STORAGE.

Informacje na temat uzyskiwania dostępu do treści multimedialnych

Podczas uzyskiwania dostępu do treści multimedialnych pamiętaj o kwestiach 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, czy są w nim dostępne aktualizacje. Dzięki temu dane z pamięci podręcznej po stronie aplikacji pozostają zsynchronizowane z danymi dostawcy po stronie systemu.

Wyniki

Gdy wykonujesz sekwencyjne odczyty plików multimedialnych za pomocą bezpośrednich ścieżek do plików, wydajność jest porównywalna z wydajnością interfejsu API MediaStore.

Jeśli jednak wykonujesz losowe odczyty i zapisy plików multimedialnych za pomocą bezpośrednich ścieżek do plików, ten proces może być nawet 2 razy wolniejszy. W takiej sytuacji zalecamy używanie interfejsu MediaStore API.

Kolumna DATA

Gdy uzyskujesz dostęp do istniejącego pliku multimedialnego, możesz w swojej logice użyć wartości z kolumny DATA. Dzieje się tak, ponieważ ta wartość ma prawidłową ścieżkę pliku. Nie oznacza to jednak, że plik jest zawsze dostępny. Przygotuj się na ewentualne błędy wejścia i wyjścia oparte na plikach.

Z kolei przy tworzeniu lub aktualizowaniu pliku multimedialnego nie używaj wartości kolumny DATA. Zamiast tego użyj wartości z kolumn DISPLAY_NAME i RELATIVE_PATH.

Woluminy pamięci masowej

Aplikacje kierowane na Androida 10 lub nowszego mogą uzyskać dostęp do unikalnej nazwy przypisanej przez system do każdego woluminu pamięci zewnętrznej. Ten system nazewnictwa pomaga skutecznie porządkować i indeksować treści oraz kontrolować miejsce przechowywania nowych plików multimedialnych.

Szczególnie przydatne są te woluminy:

  • W sekcji VOLUME_EXTERNAL widać wszystkie woluminy pamięci współdzielonej na urządzeniu. Możesz odczytywać zawartość tego woluminu syntetycznego, ale nie możesz go modyfikować.
  • VOLUME_EXTERNAL_PRIMARY oznacza podstawowy wolumin pamięci współdzielonej na urządzeniu. Możesz odczytywać i modyfikować zawartość tego woluminu.

Inne woluminy 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();

Lokalizacja, w której zostały zrobione multimedia

Niektóre zdjęcia i filmy zawierają w metadanych informacje o lokalizacji, które wskazują miejsce wykonania zdjęcia lub miejsce nagrania filmu.

Sposób uzyskiwania dostępu do informacji o lokalizacji w aplikacji zależy od tego, czy musisz uzyskać dostęp do tych informacji w przypadku zdjęcia czy filmu.

Fotografie

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

  1. Poproś o uprawnienie ACCESS_MEDIA_LOCATION w pliku manifestu aplikacji.
  2. Z obiektu MediaStore pobierz dokładne bajty zdjęcia, wywołując metodę setRequireOriginal() i przekazując 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 w sposób podany poniżej. Aplikacja nie musi prosić o dodatkowe uprawnienia do korzystania z tej klasy.

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 społecznościowe pozwalają użytkownikom udostępniać zdjęcia i filmy znajomym.

Aby udostępnić pliki multimedialne, użyj identyfikatora URI content://, zgodnie z instrukcjami podanymi w przewodniku po tworzeniu dostawcy treści.

Atrybucja plików multimedialnych w aplikacjach

Gdy pamięć o zakresie jest włączona w przypadku aplikacji kierowanej na Androida 10 lub nowszego, system przypisuje aplikację do każdego pliku multimedialnego. Określa to pliki, do których aplikacja ma dostęp, gdy nie prosi o uprawnienia do przechowywania danych. Każdy plik można przypisać tylko do 1 aplikacji. Jeśli więc aplikacja utworzy plik multimedialny przechowywany w kolekcji multimediów ze zdjęciami, filmami lub plikami audio, aplikacja będzie miała do niego dostęp.

Jeśli jednak użytkownik odinstaluje i ponownie zainstaluje Twoją aplikację, musisz poprosić READ_EXTERNAL_STORAGE o dostęp do plików, które zostały pierwotnie utworzone przez aplikację. To uprawnienie jest wymagane, ponieważ system uznaje plik za przypisany do wcześniej zainstalowanej aplikacji, a nie do nowo zainstalowanej.

Dodawanie elementu

Aby dodać element multimedialny do istniejącej kolekcji, użyj kodu podobnego do tego poniżej. Ten fragment kodu uzyskuje dostęp do woluminu VOLUME_EXTERNAL_PRIMARY na urządzeniach z Androidem 10 lub nowszym. Dzieje się tak, bo na tych urządzeniach można modyfikować zawartość woluminu tylko wtedy, gdy jest to wolumin podstawowy, zgodnie z sekcją Woluminy miejsca na dane.

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 plików multimedialnych

Jeśli Twoja aplikacja wykonuje potencjalnie czasochłonne operacje, takie jak zapisywanie w plikach multimedialnych, warto mieć wyłączny dostęp do pliku podczas jego przetwarzania. Na urządzeniach z Androidem 10 lub nowszym aplikacja może uzyskać dostęp specjalny, ustawiając wartość flagi IS_PENDING na 1. Tylko aplikacja może wyświetlać plik, dopóki aplikacja nie zmieni wartości IS_PENDING z powrotem na 0.

Poniższy fragment kodu jest uzupełnieniem poprzedniego fragmentu kodu. Ten fragment kodu pokazuje, jak użyć flagi IS_PENDING do przechowywania 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);

Podaj wskazówkę dotyczącą lokalizacji pliku

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

Jeśli aplikacja ma informacje o konkretnej lokalizacji, w której mogą być przechowywane pliki, np. w albumie ze zdjęciami o nazwie Pictures/MyVacationPictures, możesz skonfigurować MediaColumns.RELATIVE_PATH tak, aby dawał systemowi wskazówkę, gdzie zapisać nowo utworzone pliki.

Aktualizowanie elementu

Aby zaktualizować plik multimedialny należący do 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 pamięć o zakresie jest niedostępna lub nie jest włączona, proces przedstawiony we wcześniejszym fragmencie kodu działa też w przypadku plików, które nie należą do Twojej aplikacji.

Zaktualizuj w kodzie natywnym

Jeśli musisz zapisać pliki multimedialne za pomocą bibliotek natywnych, przekaż powiązany z nim deskryptor pliku z kodu opartego na języku Java lub Kotlin do kodu natywnego.

Ten fragment kodu pokazuje, jak przekazać deskryptor pliku obiektu multimedialnego do kodu natywnego 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 w innych aplikacjach

Jeśli aplikacja wykorzystuje miejsce na dane w zakresie, zwykle nie może zaktualizować pliku multimedialnego przypisanego do magazynu multimediów przez inną aplikację.

Możesz jednak uzyskać zgodę użytkownika na zmodyfikowanie pliku, wyszukując RecoverableSecurityException zwracany przez platformę. Następnie możesz poprosić użytkownika o przyznanie Twojej aplikacji uprawnień do zapisu w tym elemencie, 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);
    }
}

Wykonaj ten proces za każdym razem, gdy aplikacja chce zmodyfikować plik multimedialny, którego nie utworzyła.

Jeśli Twoja aplikacja działa w Androidzie 11 lub nowszym, możesz też zezwolić użytkownikom na przyznawanie jej uprawnień do zapisu w grupie 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 limitem miejsca na dane, prześlij prośbę o dodanie funkcji i tymczasowo zrezygnuj z ograniczonego miejsca na dane.

Usuwanie elementów

Aby usunąć produkt, którego Twoja aplikacja nie potrzebuje już ze sklepu z mediami, użyj instrukcji podobnych do tych 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 pamięć o zakresie jest niedostępna lub nie jest włączona, możesz użyć poprzedniego fragmentu kodu, aby usunąć pliki należące do innych aplikacji. Jeśli jednak włączona jest pamięć o zakresie, musisz przechwytywać RecoverableSecurityException w przypadku każdego pliku, który chcesz usunąć, zgodnie z opisem w sekcji dotyczącej aktualizowania elementów multimedialnych.

Jeśli Twoja aplikacja działa na Androidzie 11 lub nowszym, możesz pozwolić 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 limitem miejsca na dane, prześlij prośbę o dodanie funkcji i tymczasowo zrezygnuj z ograniczonego miejsca na dane.

Wykrywanie aktualizacji plików multimedialnych

Aplikacja może wymagać określenia woluminów pamięci zawierających pliki multimedialne, które aplikacje dodały lub zmodyfikowały w porównaniu z poprzednim okresem. Aby skutecznie wykrywać te zmiany, przekaż odpowiednią ilość miejsca na dane do getGeneration(). O ile wersja magazynu multimediów się nie zmieni, wartość zwrotna tej metody rośnie z czasem monotonicznie.

W szczególności dane getGeneration() są dokładniejsze niż daty w kolumnach multimediów, np. DATE_ADDED i DATE_MODIFIED. Dzieje się tak, ponieważ wartości tych kolumn mogą się zmieniać, gdy aplikacja wywoła metodę 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 zaktualizowanie ich jednocześnie. Metody te zapewniają większą spójność na różnych urządzeniach, a metody ułatwiają użytkownikom zarządzanie kolekcjami multimediów.

Metody zapewniające tę funkcję „aktualizacji zbiorczej”:

createWriteRequest()
Poproś użytkownika o przyznanie Twojej aplikacji uprawnień do zapisu w określonej grupie plików multimedialnych.
createFavoriteRequest()
Poproś użytkownika, aby oznaczył określone pliki multimedialne jako swoje „ulubione” multimedia na urządzeniu. Każda aplikacja z uprawnieniami do odczytu tego pliku może zobaczyć, że użytkownik oznaczył go jako „ulubiony”.
createTrashRequest()

Poproś użytkownika o umieszczenie określonych plików multimedialnych w koszu urządzenia. Elementy umieszczone w koszu są trwale usuwane po upływie czasu zdefiniowanego przez system.

createDeleteRequest()

Poproś użytkownika o natychmiastowe trwałe usunięcie określonych plików multimedialnych bez umieszczania ich w koszu.

Po wywołaniu dowolnej z tych metod system tworzy obiekt PendingIntent. Po wywołaniu tej intencji użytkownicy zobaczą okno z prośbą o zgodę na zaktualizowanie lub usunięcie przez aplikację określonych plików multimedialnych.

Oto przykładowa 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);

Oceń odpowiedź użytkownika. Jeśli użytkownik wyraził zgodę, możesz kontynuować działania związane z multimediami. W przeciwnym razie wyjaśnij użytkownikowi, dlaczego aplikacja potrzebuje odpowiednich uprawnień:

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żyć w createFavoriteRequest(), createTrashRequest() i createDeleteRequest().

Uprawnienia do zarządzania multimediami

Użytkownicy mogą zaufać określonej aplikacji, która będzie zarządzać multimediami, np. często edytować pliki multimedialne. 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ę musisz pokazać użytkownikowi okno z potwierdzeniem.

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

Aby to zrobić:

  1. W pliku manifestu aplikacji zadeklaruj uprawnienia MANAGE_MEDIA i READ_EXTERNAL_STORAGE.

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

  2. Pokaż w aplikacji interfejs użytkownika, aby wyjaśnić, dlaczego może chcieć przyznać aplikacji dostęp do zarządzania multimediami.

  3. Wywołaj działanie intencji ACTION_REQUEST_MANAGE_MEDIA. Gdy to zrobisz, użytkownicy zostaną przeniesieni do ekranu Aplikacje do zarządzania multimediami w ustawieniach systemu. Tutaj użytkownicy mogą przyznać dostęp specjalnej 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, zastanów się nad alternatywą dla interfejsów API MediaStore.

Praca z innymi typami plików

Jeśli 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 zgodnie z opisem w przewodniku na temat przechowywania dokumentów i innych plików oraz uzyskiwania do nich dostępu.

Udostępnianie plików w aplikacjach towarzyszących

Jeśli udostępniasz pakiet aplikacji towarzyszących, takich jak aplikacja do obsługi wiadomości i aplikacja profilu, skonfiguruj udostępnianie plików za pomocą identyfikatorów URI content://. Zalecamy też stosowanie tego przepływu pracy jako sprawdzonej metody zapewniania 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