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

Aby zapewnić lepsze wrażenia użytkownika, wiele aplikacji umożliwia użytkownikom przesyłanie treści i dostęp do multimediów dostępnych na zewnętrznym woluminie pamięci masowej. Platforma ta zapewnia zoptymalizowany indeks kolekcji multimediów, czyli magazyn multimediów, który ułatwia użytkownikom pobieranie i aktualizowanie tych plików. Nawet po odinstalowaniu aplikacji pliki te pozostają na urządzeniu użytkownika.

Selektor zdjęć

Alternatywą dla sklepu z multimediami jest selektor zdjęć na Androidzie, który jest bezpiecznym, wbudowanym narzędziem umożliwiającym użytkownikom wybieranie 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 wchodzić w interakcje z abstrakcja magazynu multimediów, użyj obiektu ContentResolver, który pobierasz 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 zewnętrznego magazynu danych i dodaje pliki multimedialne do następujących dobrze zdefiniowanych kolekcji:

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

Sklep z multimediami zawiera też kolekcję o nazwie MediaStore.Files. Jego zawartość zależy od tego, czy Twoja aplikacja korzysta z ograniczonego miejsca na dane, dostępnego w aplikacjach przeznaczonych na Androida 10 lub nowszego.

  • Jeśli ograniczona pamięć jest włączona, kolekcja zawiera tylko zdjęcia, filmy i pliki audio utworzone przez aplikację. Większość deweloperów nie musi używać uprawnienia MediaStore.Files do wyświetlania plików multimedialnych z innych aplikacji, ale jeśli masz konkretny powód, dla którego chcesz to zrobić, możesz zadeklarować uprawnienie READ_EXTERNAL_STORAGE. Zalecamy jednak, aby używać interfejsów API MediaStore do otwierania plików, które nie zostały utworzone przez Twoją aplikację.
  • Jeśli ograniczone miejsce na dane jest niedostępne lub nie jest używane, kolekcja zawiera wszystkie typy plików multimedialnych.

Proś o wymagane uprawnienia

Zanim wykonasz operacje na plikach multimedialnych, upewnij się, że aplikacja ma zadeklarowane uprawnienia potrzebne do uzyskania dostępu do tych plików. Uważaj jednak, aby nie deklarować uprawnień, których Twoja aplikacja nie potrzebuje ani nie używa.

Uprawnienia dostępu do pamięci

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

Dostęp do własnych plików multimedialnych

Na urządzeniach z Androidem 10 lub nowszym nie musisz mieć uprawnień związanych z pamięcią, aby uzyskać dostęp do plików multimedialnych należących do Twojej aplikacji i je modyfikować, w tym plików w zbiorze MediaStore.Downloads. Jeśli na przykład opracowujesz aplikację do robienia zdjęć, nie musisz prosić o uprawnienia związane z przechowywaniem danych, aby uzyskać dostęp do zdjęć, ponieważ aplikacja jest właścicielem obrazów zapisywanych w magazynie multimediów.

Dostęp do plików multimedialnych innych aplikacji

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

Plik, który można wyświetlić za pomocą zapytań MediaStore.Images, MediaStore.Video lub MediaStore.Audio, można też wyświetlić za pomocą zapytania MediaStore.Files.

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

<!-- 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 aplikacja jest używana na urządzeniu z Androidem 9 lub starszym albo jeśli tymczasowo wyłączono ograniczoną pamięć, musisz poprosić o READ_EXTERNAL_STORAGEdostęp do dowolnego pliku multimedialnego. Jeśli chcesz modyfikować pliki multimedialne, musisz poprosić o uprawnienia WRITE_EXTERNAL_STORAGE.

Storage Access Framework wymagany do uzyskiwania dostępu do plików pobranych z innych aplikacji

Jeśli aplikacja chce uzyskać dostęp do pliku w zbiorze MediaStore.Downloads, którego nie utworzyła, musi użyć interfejsu 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 wspólnego miejsca na dane.

Dostęp do lokalizacji w przypadku multimediów

Jeśli Twoja aplikacja jest kierowana na Androida 10 (poziom interfejsu API 29) lub nowszego i musi pobierać nieocenzurowane metadane EXIF z zdjęć, musisz zadeklarować uprawnienie ACCESS_MEDIA_LOCATION w pliku manifestu aplikacji, a potem poprosić o to uprawnienie w czasie działania aplikacji.

Sprawdzanie dostępności aktualizacji sklepu z multimediami

Aby uzyskać bardziej niezawodny dostęp do plików multimedialnych, zwłaszcza jeśli Twoja aplikacja przechowuje w pamięci podręcznej URI lub dane z magazynu multimediów, sprawdź, czy wersja magazynu multimediów zmieniła się w porównaniu z czasem ostatniej synchronizacji danych multimediów. Aby sprawdzić dostępność aktualizacji, zadzwoń pod numer getVersion(). Zwrócona wersja to unikalny ciąg znaków, który zmienia się za każdym razem, gdy zasób multimediów ulegnie znacznej zmianie. Jeśli zwrócona wersja różni się od ostatniej zsynchronizowanej wersji, ponownie zeskanuj i zsynchronizuj pamięć podręczną multimediów aplikacji.

Przeprowadź tę weryfikację 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.

Wysyłanie zapytania do kolekcji multimediów

Aby znaleźć media, które spełniają określony zestaw warunków, np. mają czas trwania co najmniej 5 minut, użyj instrukcji wyboru podobnej do instrukcji SQL, takiej jak ta:

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.
  • Zapisz w pamięci podręcznej indeksy kolumn, aby nie trzeba było wywoływać funkcji getColumnIndexOrThrow() za każdym razem, gdy przetwarzasz wiersz z wyników zapytania.
  • Dodaj identyfikator do identyfikatora URI treści, jak w tym przykładzie.
  • Urządzenia z Androidem 10 lub nowszym wymagają naz kolumn zdefiniowanych w interfejsie API MediaStore. Jeśli zależna biblioteka w aplikacji oczekuje nazwy kolumny, która nie jest zdefiniowana w interfejsie API, np. "MimeType", użyj parametru 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 nich, wydajniej jest wczytać wersje podglądowe (czyli miniatury) plików, a nie same pliki.

Aby wczytać miniaturę danego pliku multimedialnego, użyj funkcji loadThumbnail() i podaj rozmiar miniatury, którą chcesz wczytać, jak pokazano 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 używana do otwierania pliku multimedialnego zależy od tego, czy treści multimedialne są najlepiej reprezentowane jako deskryptor pliku, strumień plików czy bezpośrednia ścieżka pliku.

Deskryptor pliku

Aby otworzyć plik multimedialny za pomocą deskryptora pliku, użyj logiki podobnej do tej 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 tej, która jest przedstawiona 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 ułatwić płynne działanie aplikacji z bibliotekami multimediów innych firm, Android 11 (poziom interfejsu API 30) i nowsze umożliwiają korzystanie z interfejsów API innych niż MediaStore do uzyskiwania dostępu do plików multimedialnych z pamięci współdzielonej. Zamiast tego możesz uzyskać dostęp do plików multimedialnych bezpośrednio, korzystając z jednego z tych interfejsów API:

  • Interfejs API File
  • Biblioteki natywne, takie jak fopen()

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

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

Aby uzyskać dostęp do innych plików w wspólnej pamięci na urządzeniu z Androidem 10 (poziom interfejsu API 29), zalecamy tymczasowe wyłączenie ograniczonej pamięci przez ustawienie requestLegacyExternalStorage na true w pliku manifestu aplikacji. Aby uzyskać dostęp do plików multimedialnych za pomocą metod plików natywnych w Androidzie 10, musisz też poprosić o uprawnienia READ_EXTERNAL_STORAGE.

Uwagi dotyczące dostępu do treści multimedialnych

Podczas korzystania z treści multimedialnych należy wziąć pod uwagę kwestie omówione w następnych sekcjach.

Dane w pamięci podręcznej

Jeśli Twoja aplikacja przechowuje w pamięci podręcznej adresy URI lub dane ze sklepu z multimediami, okresowo sprawdzaj dostępność aktualizacji sklepu z multimediami. Dzięki temu dane w pamięci podręcznej po stronie aplikacji będą zgodne z danymi dostawcy po stronie systemu.

Wydajność

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

Jednak podczas losowego odczytu i zapisu plików multimedialnych za pomocą bezpośrednich ścieżek plików proces może być nawet dwukrotnie wolniejszy. W takich sytuacjach zalecamy użycie interfejsu API MediaStore.

Kolumna DATA

Gdy uzyskujesz dostęp do istniejącego pliku multimedialnego, możesz użyć wartości w kolumnie DATA w swojej logice. Dzieje się tak, ponieważ ta wartość ma prawidłową ścieżkę do pliku. Nie zakładaj jednak, że plik jest zawsze dostępny. Przygotuj się na obsługę wszelkich błędów we/wy plików, które mogą wystąpić.

Aby utworzyć lub zaktualizować plik multimedialny, nie używaj wartości w kolumnie DATA. Zamiast tego użyj wartości w kolumnach DISPLAY_NAMERELATIVE_PATH.

Woluminy miejsca na dane

Aplikacje przeznaczone na urządzenia z Androidem 10 lub nowszym mogą uzyskać dostęp do unikalnej nazwy, którą system przypisuje do każdego woluminu zewnętrznej pamięci masowej. Ten system nazewnictwa ułatwia organizowanie i indeksowanie treści oraz daje Ci kontrolę nad tym, gdzie są przechowywane nowe pliki multimedialne.

Warto zwrócić uwagę na te tomy:

  • Objęt VOLUME_EXTERNALpokazuje wszystkie objętości współdzielonego miejsca na dane na urządzeniu. Możesz czytać zawartość tego syntetycznego woluminu, ale nie możesz jej modyfikować.
  • Ilość VOLUME_EXTERNAL_PRIMARYreprezentuje główną objętość współdzielonego miejsca na dane na urządzeniu. Możesz czytać i modyfikować zawartość tego tomu.

Inne wolumeny możesz znaleźć, dzwoniąc na 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 zarejestrowane multimedia

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

Sposób uzyskiwania dostępu do tych informacji o lokalizacji w aplikacji zależy od tego, czy chcesz uzyskać dostęp do informacji o lokalizacji 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 przyznanie uprawnienia ACCESS_MEDIA_LOCATION w pliku manifestu aplikacji.
  2. Aby uzyskać dokładne bajty zdjęcia z obiektu MediaStore, wywołaj funkcję setRequireOriginal() i przekaż identyfikator URI zdjęcia, jak pokazano 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. Aplikacja nie musi prosić o żadne dodatkowe uprawnienia, aby korzystać 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 nawzajem plików multimedialnych. Na przykład aplikacje społecznościowe umożliwiają użytkownikom udostępnianie zdjęć i filmów znajomym.

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

Atrybucja aplikacji plików multimedialnych

Gdy wyspecjalizowane miejsce na dane jest włączone w przypadku aplikacji kierowanej na Androida 10 lub nowszego, system przypisuje aplikację do każdego pliku multimedialnego, co określa pliki, do których aplikacja może uzyskać dostęp, gdy nie poprosi o żadne uprawnienia do miejsca na dane. Każdy plik może być przypisany tylko do jednej aplikacji. Jeśli więc Twoja aplikacja utworzy plik multimedialny, który jest przechowywany w zbiorze multimediów (zdjęć, filmów lub plików audio), aplikacja ma do niego dostęp.

Jeśli użytkownik odinstaluje aplikację i ponownie ją zainstaluje, musisz poprosić o dostęp do plików utworzonych przez Twoją aplikację (READ_EXTERNAL_STORAGE). To żądanie uprawnień jest wymagane, ponieważ system uznaje plik za przypisany do wcześniej zainstalowanej wersji aplikacji, a nie do 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 modyfikować zawartość woluminu tylko wtedy, gdy jest to wolumin podstawowy, jak opisano 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łączanie stanu oczekujący w przypadku plików multimedialnych

Jeśli aplikacja wykonuje operacje, które mogą być czasochłonne, np. zapisywanie plików multimedialnych, warto mieć wyłączny dostęp do pliku podczas jego przetwarzania. Na urządzeniach z Androidem 10 lub nowszym aplikacja może uzyskać ten dostęp wyłączny, ustawiając wartość flagi IS_PENDING na 1. Dopóki aplikacja nie zmieni wartości parametru IS_PENDING z powrotem na 0, tylko ona będzie mogła wyświetlać plik.

Ten fragment kodu opiera się na poprzednim fragmencie kodu. Ten fragment kodu pokazuje, jak użyć flagi IS_PENDING podczas 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);

Podawanie wskazówki dotyczącej lokalizacji pliku

Gdy aplikacja przechowuje multimedia na urządzeniu z Androidem 10, domyślnie są one uporządkowane według 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 aplikacja wie, gdzie można zapisać pliki, np. w albumie zdjęć o nazwie Pictures/MyVacationPictures, możesz ustawić parametr MediaColumns.RELATIVE_PATH, aby podać 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 ograniczona pamięć masowa jest niedostępna lub nie jest włączona, proces pokazany w poprzednim fragmencie kodu działa też w przypadku plików, które nie należą do aplikacji.

Aktualizacja w kodzie natywnym

Jeśli musisz zapisywać pliki multimedialne za pomocą bibliotek natywnych, przekaż powiązany z tym plikiem deskryptor pliku z kodu na podstawie Javy lub Kotlina 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.
}

aktualizować pliki multimedialne innych aplikacji.

Jeśli aplikacja korzysta z ograniczonego miejsca na dane, zwykle nie może zaktualizować pliku multimedialnego, który został przesłany do magazynu multimediów przez inną aplikację.

Możesz jednak uzyskać zgodę użytkownika na modyfikację pliku, przechwytując RecoverableSecurityException, który platforma zwraca. Następnie możesz poprosić użytkownika o przyznanie aplikacji uprawnień do zapisu w danym 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);
    }
}

Wykonuj tę procedurę za każdym razem, gdy aplikacja musi zmodyfikować plik multimedialny, który nie został przez nią utworzony.

Jeśli Twoja aplikacja działa na Androidzie 11 lub nowszym, możesz też zezwolić użytkownikom na przyznanie jej dostępu do zapisu do grupy plików multimedialnych. Użyj metody createWriteRequest(), jak opisano w sekcji Zarządzanie grupami plików multimedialnych.

Jeśli Twoja aplikacja ma inne zastosowanie, które nie jest objęte ograniczonym dostępem do pamięci, prześlij prośbę o dodanie funkcjitymczasowo wyłącz ograniczony dostęp do pamięci.

Usuwanie elementu

Aby usunąć element, którego aplikacja już nie potrzebuje, z magazynu multimediów, użyj logiki podobnej do tej w podanym niżej 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 ograniczona pamięć jest niedostępna lub nie jest włączona, możesz użyć powyższego fragmentu kodu, aby usunąć pliki należące do innych aplikacji. Jeśli jednak masz włączone ograniczone przechowywanie, musisz przechwycić zdarzenie RecoverableSecurityException dla każdego pliku, który aplikacja chce usunąć, zgodnie z opisem w sekcji Aktualizowanie elementów multimediów.

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 metody createDeleteRequest(), jak opisano w sekcji Zarządzanie grupami plików multimedialnych.

Jeśli Twoja aplikacja ma inne zastosowanie, które nie jest objęte ograniczonym dostępem do pamięci, prześlij prośbę o dodanie funkcjitymczasowo wyłącz ograniczony dostęp do pamięci.

Wykrywanie zmian w plikach multimedialnych

Aplikacja może potrzebować identyfikacji woluminów pamięci masowej zawierających pliki multimedialne, które zostały dodane lub zmodyfikowane przez aplikacje w porównaniu z wcześniejszym stanem. Aby wykrywać te zmiany w najbardziej niezawodny sposób, przekaż interesujący Cię wolumen miejsca na dane do funkcji getGeneration(). Dopóki wersja magazynu multimediów się nie zmieni, wartość zwracana przez tę metodę będzie monotonicznie wzrastać z czasem.

Szczególnie getGeneration() jest bardziej niezawodny niż daty w kolumnach multimediów, takich jak DATE_ADDEDDATE_MODIFIED. Dzieje się tak, ponieważ wartości w kolumnach z multimediami mogą się zmieniać, gdy aplikacja wywołuje funkcję setLastModified() lub gdy użytkownik zmienia zegar systemowy.

Zarządzanie grupami plików multimedialnych

W Androidzie 11 i nowszych możesz poprosić użytkownika o wybranie grupy plików multimedialnych, a potem zaktualizować te pliki w jednej operacji. Te metody zapewniają większą spójność na różnych urządzeniach i ułatwiają użytkownikom zarządzanie kolekcjami multimediów.

Metody, które umożliwiają tę funkcję „zbiorcze aktualizacji”, to:

createWriteRequest()
Poproś użytkownika o przyznanie aplikacji dostępu do zapisu w określonej grupie plików multimedialnych.
createFavoriteRequest()
Poproś użytkownika o oznaczenie określonych plików multimedialnych jako „ulubionych” na urządzeniu. Każda aplikacja z dostępem do odczytu tego pliku może zobaczyć, że użytkownik oznaczył go jako „ulubiony”.
createTrashRequest()

Poproś użytkownika o przeniesienie wskazanych plików multimedialnych do kosza na urządzeniu. Elementy w koszu są trwale usuwane po określonym przez system czasie.

createDeleteRequest()

Poproś użytkownika o bezpośrednie trwałe usunięcie określonych plików multimedialnych bez umieszczania ich wcześniej w koszy.

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

Oto przykład tworzenia wywołania funkcji 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);

Sprawdź odpowiedź użytkownika. Jeśli użytkownik wyraził zgodę, kontynuuj operację związaną z mediami. W przeciwnym razie wyjaśnij użytkownikowi, dlaczego aplikacja potrzebuje tego 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 wzoru możesz używać w przypadku atrybutów createFavoriteRequest(), createTrashRequest()createDeleteRequest().

Uprawnienia do zarządzania multimediami

Użytkownicy mogą ufać danej aplikacji, że będzie ona 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, musi wyświetlać użytkownikowi okno potwierdzenia za każdym razem, gdy próbuje zmodyfikować lub usunąć plik.

Jeśli Twoja aplikacja jest kierowana na Androida 12 (poziom API 31) lub nowszego, możesz poprosić użytkowników o przyznanie aplikacji uprawnień specjalnych zarządzania multimediami. To uprawnienie umożliwia aplikacji wykonywanie tych czynności bez konieczności wyświetlania użytkownikowi zapytania o pozwolenie na każdą operację na pliku:

Aby to zrobić:

  1. Zadeklaruj uprawnienia MANAGE_MEDIAREAD_EXTERNAL_STORAGE w pliku manifestu aplikacji.

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

  2. Wyświetl w aplikacji interfejs użytkownika, aby wyjaśnić, dlaczego warto zezwolić aplikacji na zarządzanie multimediami.

  3. Wywołaj działanie intencyjny ACTION_REQUEST_MANAGE_MEDIA. Użytkownicy są przekierowywani do ekranu Aplikacje do zarządzania multimediami w ustawieniach systemu. Tutaj użytkownicy mogą przyznać aplikacji specjalny dostęp.

Przypadki użycia wymagające alternatywy dla magazynu multimediów

Jeśli Twoja aplikacja pełni głównie jedną z tych ról, rozważ użycie 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 intencyjnego ACTION_OPEN_DOCUMENT, zgodnie z opisem w przewodniku dotyczącym przechowywania i otwierania dokumentów oraz innych plików.

Udostępnianie plików w aplikacjach towarzyszących

Jeśli udostępniasz zestaw aplikacji towarzyszących, takich jak aplikacja do obsługi wiadomości i aplikacja do obsługi profilu, skonfiguruj udostępnianie plików za pomocą adresów URI content://. Zalecamy też ten proces jako sprawdzoną metodę zapewniania bezpieczeństwa.

Dodatkowe materiały

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

Próbki

Filmy