Na urządzeniach z Androidem 4.4 (poziom interfejsu API 19) lub nowszym aplikacja może wchodzić w interakcje z dostawcą dokumentów, w tym z zewnętrznymi woluminami pamięci i pamięcią w chmurze, za pomocą Storage Access Framework. Ta platforma umożliwia użytkownikom interakcję z selektorem systemowym, aby wybrać dostawcę dokumentów i określić konkretne dokumenty oraz inne pliki, które aplikacja może tworzyć, otwierać lub modyfikować.
Ponieważ użytkownik bierze udział w wybieraniu plików lub katalogów, do których aplikacja może uzyskać dostęp, ten mechanizm nie wymaga żadnych uprawnień systemowych, a kontrola użytkownika i ochrona prywatności są większe. Dodatkowo te pliki, które są przechowywane poza katalogiem konkretnej aplikacji i poza magazynem multimediów, pozostają na urządzeniu po odinstalowaniu aplikacji.
Korzystanie z platformy obejmuje te kroki:
- Aplikacja wywołuje intencję zawierającą działanie związane z pamięcią. To działanie odpowiada konkretnemu przypadkowi użycia, który udostępnia platforma.
- Użytkownik widzi selektor systemowy, który umożliwia mu przeglądanie dostawcy dokumentów i wybieranie lokalizacji lub dokumentu, w którym ma zostać wykonane działanie związane z pamięcią.
- Aplikacja uzyskuje uprawnienia do odczytu i zapisu identyfikatora URI, który reprezentuje wybraną przez użytkownika lokalizację lub dokument. Za pomocą tego identyfikatora URI aplikacja może wykonywać operacje w wybranej lokalizacji.
Aby obsługiwać dostęp do plików multimedialnych na urządzeniach z Androidem 9 (poziom interfejsu API 28) lub starszym, zadeklaruj uprawnienie READ_EXTERNAL_STORAGE
i ustaw wartość maxSdkVersion
na 28
.
W tym przewodniku opisujemy różne przypadki użycia, które umożliwiają pracę z plikami i innymi dokumentami. Wyjaśniono w nim również, jak wykonywać operacje na lokalizacji wybranej przez użytkownika.
Przypadki użycia dostępu do dokumentów i innych plików
Platforma Storage Access Framework obsługuje te przypadki użycia dostępu do plików i innych dokumentów:
- Tworzenie nowego pliku
- Działanie intencji
ACTION_CREATE_DOCUMENT
umożliwia użytkownikom zapisanie pliku w określonej lokalizacji. - Otwieranie dokumentu lub pliku
- Działanie intencji
ACTION_OPEN_DOCUMENT
umożliwia użytkownikom wybranie konkretnego dokumentu lub pliku do otwarcia.
- Przyznawanie dostępu do zawartości katalogu
- Działanie intencji
ACTION_OPEN_DOCUMENT_TREE
, dostępne na Androidzie 5.0 (poziom API 21) i nowszym, umożliwia użytkownikom wybór konkretnego katalogu, co daje aplikacji dostęp do wszystkich plików i podkatalogów w tym katalogu.
W sekcjach poniżej znajdziesz wskazówki dotyczące konfigurowania poszczególnych przypadków użycia.
Tworzenie nowego pliku
Użyj działania intencji
ACTION_CREATE_DOCUMENT
intent, aby wczytać okno wyboru plików i umożliwić użytkownikowi wybranie
lokalizacji, w której mają zostać zapisane treści pliku. Ten proces jest podobny do tego, który jest używany w oknach „Zapisz jako” w innych systemach operacyjnych.
Uwaga: ACTION_CREATE_DOCUMENT
nie można zastąpić istniejącego pliku. Jeśli aplikacja spróbuje zapisać plik o tej samej nazwie, system doda na końcu nazwy pliku liczbę w nawiasach.
Jeśli na przykład aplikacja próbuje zapisać plik o nazwie confirmation.pdf
w katalogu, w którym jest już plik o tej nazwie, system zapisze nowy plik pod nazwą confirmation(1).pdf
.
Podczas konfigurowania intencji określ nazwę i typ MIME pliku, a opcjonalnie także identyfikator URI pliku lub katalogu, który selektor plików ma wyświetlać po pierwszym wczytaniu, używając dodatkowej intencji EXTRA_INITIAL_URI
.
Ten fragment kodu pokazuje, jak utworzyć i wywołać intencję utworzenia pliku:
Kotlin
// Request code for creating a PDF document. const val CREATE_FILE = 1 private fun createFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" putExtra(Intent.EXTRA_TITLE, "invoice.pdf") // Optionally, specify a URI for the directory that should be opened in // the system file picker before your app creates the document. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, CREATE_FILE) }
Java
// Request code for creating a PDF document. private static final int CREATE_FILE = 1; private void createFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf"); // Optionally, specify a URI for the directory that should be opened in // the system file picker when your app creates the document. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, CREATE_FILE); }
Otwórz plik
Aplikacja może używać dokumentów jako jednostki pamięci, w której użytkownicy wprowadzają dane, które mogą chcieć udostępnić innym lub zaimportować do innych dokumentów. Przykłady to otwarcie dokumentu zwiększającego produktywność lub książki zapisanej jako plik EPUB.
W takich przypadkach zezwól użytkownikowi na wybranie pliku do otwarcia, wywołując intencję
ACTION_OPEN_DOCUMENT
otwierającą aplikację systemową do wybierania plików. Aby wyświetlać tylko typy plików obsługiwane przez aplikację, określ typ MIME. Opcjonalnie możesz też określić URI pliku, który ma być wyświetlany w selektorze plików po pierwszym wczytaniu, za pomocą dodatkowego parametru intencji EXTRA_INITIAL_URI
.
Ten fragment kodu pokazuje, jak utworzyć i wywołać intencję otwarcia dokumentu PDF:
Kotlin
// Request code for selecting a PDF document. const val PICK_PDF_FILE = 2 fun openFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" // Optionally, specify a URI for the file that should appear in the // system file picker when it loads. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, PICK_PDF_FILE) }
Java
// Request code for selecting a PDF document. private static final int PICK_PDF_FILE = 2; private void openFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); // Optionally, specify a URI for the file that should appear in the // system file picker when it loads. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, PICK_PDF_FILE); }
Ograniczenia dostępu
Na Androidzie 11 (poziom API 30) i nowszych nie możesz używać działania intencji ACTION_OPEN_DOCUMENT
, aby poprosić użytkownika o wybranie poszczególnych plików z tych katalogów:
- Katalog
Android/data/
i wszystkie podkatalogi. - Katalog
Android/obb/
i wszystkie podkatalogi.
Przyznawanie dostępu do zawartości katalogu
Aplikacje do zarządzania plikami i tworzenia multimediów zwykle zarządzają grupami plików w hierarchii katalogów. Aby udostępnić tę funkcję w aplikacji, użyj działania intencji
ACTION_OPEN_DOCUMENT_TREE
, które umożliwia użytkownikowi przyznanie dostępu do całego drzewa katalogów, z pewnymi wyjątkami od Androida 11 (poziom API 30). Aplikacja będzie wtedy mieć dostęp do wszystkich plików w wybranym katalogu i jego podkatalogach.
Gdy używasz ACTION_OPEN_DOCUMENT_TREE
, aplikacja uzyskuje dostęp tylko do plików w katalogu wybranym przez użytkownika. Nie masz dostępu do plików innych aplikacji, które znajdują się poza wybranym przez użytkownika katalogiem. Ten kontrolowany przez użytkownika dostęp pozwala mu wybrać dokładnie te treści, które chce udostępnić aplikacji.
Opcjonalnie możesz określić URI katalogu, który ma być wyświetlany przez selektor plików po pierwszym wczytaniu, za pomocą dodatkowego parametru intencji EXTRA_INITIAL_URI
.
Ten fragment kodu pokazuje, jak utworzyć i wywołać intencję otwierania katalogu:
Kotlin
fun openDirectory(pickerInitialUri: Uri) { // Choose a directory using the system's file picker. val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { // Optionally, specify a URI for the directory that should be opened in // the system file picker when it loads. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, your-request-code) }
Java
public void openDirectory(Uri uriToLoad) { // Choose a directory using the system's file picker. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // Optionally, specify a URI for the directory that should be opened in // the system file picker when it loads. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad); startActivityForResult(intent, your-request-code); }
Ograniczenia dostępu
Na Androidzie 11 (poziom interfejsu API 30) i nowszym nie możesz używać działania intencji ACTION_OPEN_DOCUMENT_TREE
do żądania dostępu do tych katalogów:
- Katalog główny woluminu pamięci wewnętrznej.
- Katalog główny każdego woluminu karty SD, który producent urządzenia uważa za niezawodny, niezależnie od tego, czy karta jest emulowana, czy wyjmowana. Niezawodny wolumin to taki, do którego aplikacja może uzyskać dostęp przez większość czasu.
- Katalog
Download
.
Ponadto w Androidzie 11 (poziom interfejsu API 30) i nowszym nie możesz używać działania intencji ACTION_OPEN_DOCUMENT_TREE
, aby poprosić użytkownika o wybranie poszczególnych plików z tych katalogów:
- Katalog
Android/data/
i wszystkie podkatalogi. - Katalog
Android/obb/
i wszystkie podkatalogi.
Wykonywanie operacji w wybranej lokalizacji
Gdy użytkownik wybierze plik lub katalog za pomocą selektora plików systemu, możesz pobrać identyfikator URI wybranego elementu za pomocą tego kodu w onActivityResult()
:
Kotlin
override fun onActivityResult( requestCode: Int, resultCode: Int, resultData: Intent?) { if (requestCode == your-request-code && resultCode == Activity.RESULT_OK) { // The result data contains a URI for the document or directory that // the user selected. resultData?.data?.also { uri -> // Perform operations on the document using its URI. } } }
Java
@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == your-request-code && resultCode == Activity.RESULT_OK) { // The result data contains a URI for the document or directory that // the user selected. Uri uri = null; if (resultData != null) { uri = resultData.getData(); // Perform operations on the document using its URI. } } }
Uzyskując odniesienie do identyfikatora URI wybranego elementu, aplikacja może wykonywać na nim różne operacje. Możesz na przykład uzyskać dostęp do metadanych elementu, edytować go w miejscu i usunąć.
W sekcjach poniżej znajdziesz informacje o tym, jak wykonywać działania na plikach wybranych przez użytkownika.
Określanie operacji obsługiwanych przez dostawcę
Różni dostawcy treści umożliwiają wykonywanie różnych operacji na dokumentach, takich jak kopiowanie dokumentu czy wyświetlanie jego miniatury. Aby określić, które operacje obsługuje dany dostawca, sprawdź wartość Document.COLUMN_FLAGS
.
Interfejs aplikacji może wtedy wyświetlać tylko opcje obsługiwane przez dostawcę.
Utrwalanie uprawnień
Gdy aplikacja otwiera plik do odczytu lub zapisu, system przyznaje jej uprawnienia URI do tego pliku, które obowiązują do ponownego uruchomienia urządzenia użytkownika. Załóżmy jednak, że Twoja aplikacja służy do edycji obrazów i chcesz, aby użytkownicy mieli dostęp do 5 ostatnio edytowanych obrazów bezpośrednio z aplikacji. Jeśli urządzenie użytkownika zostało ponownie uruchomione, musisz przekierować go do selektora systemowego, aby mógł znaleźć pliki.
Aby zachować dostęp do plików po ponownym uruchomieniu urządzenia i zapewnić użytkownikowi lepsze wrażenia, aplikacja może „przejąć” przyznane przez system uprawnienie do trwałego identyfikatora URI, jak pokazano w tym fragmencie kodu:
Kotlin
val contentResolver = applicationContext.contentResolver val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION // Check for the freshest data. contentResolver.takePersistableUriPermission(uri, takeFlags)
Java
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Check for the freshest data. getContentResolver().takePersistableUriPermission(uri, takeFlags);
Sprawdzanie metadanych dokumentu
Gdy masz identyfikator URI dokumentu, uzyskujesz dostęp do jego metadanych. Ten fragment kodu pobiera metadane dokumentu określonego przez identyfikator URI i rejestruje je:
Kotlin
val contentResolver = applicationContext.contentResolver fun dumpImageMetaData(uri: Uri) { // The query, because it only applies to a single document, returns only // one row. There's no need to filter, sort, or select fields, // because we want all fields for one document. val cursor: Cursor? = contentResolver.query( uri, null, null, null, null, null) cursor?.use { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. if (it.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. val displayName: String = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) Log.i(TAG, "Display Name: $displayName") val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE) // If the size is unknown, the value stored is null. But because an // int can't be null, the behavior is implementation-specific, // and unpredictable. So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. val size: String = if (!it.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. it.getString(sizeIndex) } else { "Unknown" } Log.i(TAG, "Size: $size") } } }
Java
public void dumpImageMetaData(Uri uri) { // The query, because it only applies to a single document, returns only // one row. There's no need to filter, sort, or select fields, // because we want all fields for one document. Cursor cursor = getActivity().getContentResolver() .query(uri, null, null, null, null, null); try { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. if (cursor != null && cursor.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. String displayName = cursor.getString( cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); Log.i(TAG, "Display Name: " + displayName); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); // If the size is unknown, the value stored is null. But because an // int can't be null, the behavior is implementation-specific, // and unpredictable. So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. String size = null; if (!cursor.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. size = cursor.getString(sizeIndex); } else { size = "Unknown"; } Log.i(TAG, "Size: " + size); } } finally { cursor.close(); } }
Otwieranie dokumentu
Dzięki odwołaniu do identyfikatora URI dokumentu możesz otworzyć dokument w celu dalszego przetwarzania. W tej sekcji znajdziesz przykłady otwierania mapy bitowej i strumienia wejściowego.
Bitmapa
Poniższy fragment kodu pokazuje, jak otworzyć plik Bitmap
na podstawie jego URI:
Kotlin
val contentResolver = applicationContext.contentResolver @Throws(IOException::class) private fun getBitmapFromUri(uri: Uri): Bitmap { val parcelFileDescriptor: ParcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r") val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor) parcelFileDescriptor.close() return image }
Java
private Bitmap getBitmapFromUri(Uri uri) throws IOException { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image; }
Po otwarciu mapy bitowej możesz ją wyświetlić w ImageView
.
Strumień wejściowy
Poniższy fragment kodu pokazuje, jak otworzyć obiekt InputStream na podstawie jego adresu URI. W tym fragmencie wiersze pliku są odczytywane do ciągu znaków:
Kotlin
val contentResolver = applicationContext.contentResolver @Throws(IOException::class) private fun readTextFromUri(uri: Uri): String { val stringBuilder = StringBuilder() contentResolver.openInputStream(uri)?.use { inputStream -> BufferedReader(InputStreamReader(inputStream)).use { reader -> var line: String? = reader.readLine() while (line != null) { stringBuilder.append(line) line = reader.readLine() } } } return stringBuilder.toString() }
Java
private String readTextFromUri(Uri uri) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader( new InputStreamReader(Objects.requireNonNull(inputStream)))) { String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } } return stringBuilder.toString(); }
Edytowanie dokumentu
Za pomocą Storage Access Framework możesz edytować dokument tekstowy w miejscu.
Ten fragment kodu zastępuje zawartość dokumentu reprezentowanego przez podany identyfikator URI:
Kotlin
val contentResolver = applicationContext.contentResolver private fun alterDocument(uri: Uri) { try { contentResolver.openFileDescriptor(uri, "w")?.use { FileOutputStream(it.fileDescriptor).use { it.write( ("Overwritten at ${System.currentTimeMillis()}\n") .toByteArray() ) } } } catch (e: FileNotFoundException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } }
Java
private void alterDocument(Uri uri) { try { ParcelFileDescriptor pfd = getActivity().getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() + "\n").getBytes()); // Let the document provider know you're done by closing the stream. fileOutputStream.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Usuwanie dokumentu
Jeśli masz identyfikator URI dokumentu, a dokument Document.COLUMN_FLAGS
zawiera SUPPORTS_DELETE
, możesz go usunąć. Na przykład:
Kotlin
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
Java
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);
Pobieranie równoważnego identyfikatora URI multimediów
Metoda
getMediaUri()
zwraca identyfikator URI magazynu multimediów, który jest odpowiednikiem podanego identyfikatora URI dostawcy dokumentów. Oba identyfikatory URI odnoszą się do tego samego elementu. Korzystając z identyfikatora URI magazynu multimediów, możesz łatwiej uzyskać dostęp do plików multimedialnych z pamięci współdzielonej.
Metoda getMediaUri()
obsługuje identyfikatory URI ExternalStorageProvider
. Na Androidzie 12 (poziom interfejsu API 31) i nowszych wersjach ta metoda obsługuje też identyfikatory URI MediaDocumentsProvider
.
Otwieranie pliku wirtualnego
Na Androidzie 7.0 (poziom interfejsu API 25) i nowszym aplikacja może korzystać z plików wirtualnych udostępnianych przez Storage Access Framework. Chociaż pliki wirtualne nie mają reprezentacji binarnej, aplikacja może otwierać ich zawartość, przekształcając je w inny typ pliku lub wyświetlając je za pomocą działania intencji ACTION_VIEW
.
Aby otwierać pliki wirtualne, aplikacja kliencka musi zawierać specjalną logikę do ich obsługi. Jeśli chcesz uzyskać reprezentację pliku w postaci bajtów, np. aby wyświetlić podgląd pliku, musisz poprosić dostawcę dokumentów o alternatywny typ MIME.
Gdy użytkownik dokona wyboru, użyj identyfikatora URI w danych wyników, aby określić, czy plik jest wirtualny, jak pokazano w tym fragmencie kodu:
Kotlin
private fun isVirtualFile(uri: Uri): Boolean { if (!DocumentsContract.isDocumentUri(this, uri)) { return false } val cursor: Cursor? = contentResolver.query( uri, arrayOf(DocumentsContract.Document.COLUMN_FLAGS), null, null, null ) val flags: Int = cursor?.use { if (cursor.moveToFirst()) { cursor.getInt(0) } else { 0 } } ?: 0 return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0 }
Java
private boolean isVirtualFile(Uri uri) { if (!DocumentsContract.isDocumentUri(this, uri)) { return false; } Cursor cursor = getContentResolver().query( uri, new String[] { DocumentsContract.Document.COLUMN_FLAGS }, null, null, null); int flags = 0; if (cursor.moveToFirst()) { flags = cursor.getInt(0); } cursor.close(); return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0; }
Po sprawdzeniu, czy dokument jest plikiem wirtualnym, możesz przekształcić go w inny typ MIME, np. "image/png"
. Poniższy fragment kodu pokazuje, jak sprawdzić, czy plik wirtualny może być reprezentowany jako obraz, a jeśli tak, pobrać z niego strumień wejściowy:
Kotlin
@Throws(IOException::class) private fun getInputStreamForVirtualFile( uri: Uri, mimeTypeFilter: String): InputStream { val openableMimeTypes: Array<String>? = contentResolver.getStreamTypes(uri, mimeTypeFilter) return if (openableMimeTypes?.isNotEmpty() == true) { contentResolver .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null) .createInputStream() } else { throw FileNotFoundException() } }
Java
private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter) throws IOException { ContentResolver resolver = getContentResolver(); String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter); if (openableMimeTypes == null || openableMimeTypes.length < 1) { throw new FileNotFoundException(); } return resolver .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null) .createInputStream(); }
Dodatkowe materiały
Więcej informacji o przechowywaniu dokumentów i innych plików oraz uzyskiwaniu do nich dostępu znajdziesz w tych materiałach.
Próbki
- ActionOpenDocument, dostępny na GitHubie.
- ActionOpenDocumentTree, dostępny w GitHubie.