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 wolnościami pamięci zewnętrznej i miejscem na dane w chmurze, za pomocą platformy Storage Access Framework. Dzięki niej użytkownicy mogą korzystać z selektora systemowego, aby wybrać dostawcę dokumentów oraz konkretne dokumenty i inne pliki do utworzenia, otwarcia lub zmodyfikowania przez aplikację.
To użytkownik decyduje o wyborze plików lub katalogów, do których aplikacja ma dostęp, dlatego ten mechanizm nie wymaga żadnych uprawnień systemu, a dodatkowo kontrola użytkowników i prywatność są większe. Dodatkowo te pliki, które są przechowywane poza katalogiem dla aplikacji i poza sklepem multimedialnym, pozostają na urządzeniu po odinstalowaniu aplikacji.
Aby korzystać z platformy, wykonaj te czynności:
- Aplikacja wywołuje intencję, która zawiera działanie związane z pamięcią. To działanie odpowiada konkretnemu przypadkowi użycia udostępnianemu przez platformę.
- Użytkownik widzi selektor systemowy, który umożliwia przeglądanie dostawcy dokumentów i wybieranie lokalizacji lub dokumentu, w którym ma miejsce działanie związane z przechowywaniem.
- Aplikacja uzyskuje uprawnienia do odczytu i zapisu do identyfikatora URI, który reprezentuje wybraną lokalizację lub dokument użytkownika. Za pomocą tego identyfikatora URI aplikacja może wykonywać operacje w wybranej lokalizacji.
Aby zapewnić obsługę dostępu do plików multimedialnych na urządzeniach z Androidem 9 (poziom interfejsu API 28) lub starszym, zadeklaruj uprawnienia READ_EXTERNAL_STORAGE
i ustaw maxSdkVersion
na 28
.
W tym przewodniku omawiamy różne przypadki użycia obsługiwane przez platformę w przypadku pracy z plikami i innymi dokumentami. Wyjaśniamy także, jak wykonywać operacje w lokalizacji wybranej przez użytkownika.
Przypadki użycia dostępu do dokumentów i innych plików
Platforma Storage Access Framework obsługuje opisane poniżej przypadki użycia do uzyskiwania dostępu do plików i innych dokumentów.
- Tworzenie nowego pliku
- Czynność
ACTION_CREATE_DOCUMENT
pozwala użytkownikom zapisać plik w określonej lokalizacji. - Otwieranie dokumentu lub pliku
- Czynność
ACTION_OPEN_DOCUMENT
pozwala użytkownikom wybrać konkretny dokument lub plik do otwarcia. - Przyznawanie dostępu do zawartości katalogu
- Działanie intencji
ACTION_OPEN_DOCUMENT_TREE
, dostępne na Androidzie 5.0 (poziom interfejsu API 21) i nowszych, pozwala użytkownikom wybrać konkretny katalog, 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
, aby wczytać systemowy selektor plików i umożliwić użytkownikowi wybranie lokalizacji, w której będzie można zapisać zawartość pliku. Ten proces przypomina okno dialogowe „Zapisz jako” używane w innych systemach operacyjnych.
Uwaga: ACTION_CREATE_DOCUMENT
nie może zastąpić istniejącego pliku. Jeśli aplikacja spróbuje zapisać plik o tej samej nazwie, system doda na jego końcu 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 o nazwie confirmation(1).pdf
.
Podczas konfigurowania intencji określ nazwę pliku i typ MIME oraz opcjonalnie wskaż identyfikator URI pliku lub katalogu, który selektor plików ma wyświetlić po pierwszym wczytaniu, korzystając z 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 wpisują dane, które chcą udostępnić innym użytkownikom lub importować do innych dokumentów. Przykładami takich treści są otwieranie dokumentu zwiększającego produktywność lub książki zapisanej jako plik EPUB.
W takich przypadkach pozwól użytkownikowi wybrać plik do otwarcia, wywołując intencję ACTION_OPEN_DOCUMENT
, która spowoduje otwarcie systemowej aplikacji z selektorem plików. Aby wyświetlić tylko typy plików obsługiwane przez Twoją aplikację, określ typ MIME. Korzystając z dodatkowej intencji EXTRA_INITIAL_URI
, możesz też opcjonalnie określić identyfikator URI pliku, który ma być wyświetlany w selektorze plików po pierwszym wczytaniu.
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
W Androidzie 11 (poziom interfejsu API 30) i nowszych nie można 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 swojej 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, począwszy od Androida 11 (poziom API 30). Aplikacja może wtedy uzyskać dostęp do dowolnego pliku w wybranym katalogu i dowolnych jego podkatalogach.
Gdy używasz ACTION_OPEN_DOCUMENT_TREE
, aplikacja uzyskuje dostęp tylko do plików w katalogu, który użytkownik wybierze. Nie masz dostępu do plików innych aplikacji znajdujących się poza tym katalogiem wybranym przez użytkownika. Dzięki temu dostępowi użytkownicy mogą dokładnie wybrać, jakie treści chcą udostępnić Twojej aplikacji.
Opcjonalnie możesz użyć dodatkowej intencji EXTRA_INITIAL_URI
, aby określić identyfikator URI katalogu, który ma być wyświetlany przez selektor plików po pierwszym wczytaniu.
Ten fragment kodu pokazuje, jak utworzyć i wywołać intencję otwarcia 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
W Androidzie 11 (poziom interfejsu API 30) i nowszych nie można używać działania intencji ACTION_OPEN_DOCUMENT_TREE
, aby poprosić o dostęp do tych katalogów:
- Katalog główny woluminu pamięci wewnętrznej.
- Katalog główny woluminu karty SD, który producent urządzenia uznaje za niezawodny, bez względu na to, czy karta jest emulowana czy wyjmowana. Niezawodny wolumin to taki, do którego aplikacja ma zazwyczaj dostęp przez większość czasu.
- Katalog
Download
.
Ponadto w Androidzie 11 (poziom interfejsu API 30) i nowszych nie można 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 na wybranej lokalizacji
Gdy użytkownik wybierze plik lub katalog za pomocą systemowego selektora plików, możesz pobrać jego identyfikator URI, korzystając z 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. } } }
Dzięki uzyskaniu odniesienia do identyfikatora URI wybranego elementu aplikacja może wykonać na nim kilka operacji. Można na przykład uzyskać dostęp do metadanych elementu, edytować go i usunąć.
W sekcjach poniżej dowiesz się, jak wykonywać działania na plikach wybranych przez użytkownika.
Określ operacje obsługiwane przez dostawcę
Różni dostawcy treści pozwalają na wykonywanie na dokumentach odmiennych operacji, takich jak kopiowanie dokumentu lub wyświetlanie jego miniatury. Aby określić, które operacje obsługuje dany dostawca, sprawdź wartość Document.COLUMN_FLAGS
.
Interfejs aplikacji może zawierać tylko opcje obsługiwane przez dostawcę.
Zachowaj uprawnienia
Gdy aplikacja otwiera plik do odczytu lub zapisu, system przyzna jej uprawnienia do tego pliku przyznane przez identyfikator URI. Trwa ono do momentu 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 w aplikacji. Jeśli urządzenie użytkownika zostało ponownie uruchomione, musisz odesłać go do selektora systemowego, aby znalazł pliki.
Aby zachować dostęp do plików na różnych urządzeniach podczas ponownego uruchamiania i zapewnić większą wygodę użytkownikom, aplikacja może „przyjąć” trwałe uprawnienia identyfikatora URI przyznane przez system, jak widać 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
Dostęp do metadanych dokumentu uzyskasz, jeśli masz identyfikator URI dokumentu. Ten fragment 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
Wykorzystując odniesienie do identyfikatora URI dokumentu, możesz go otworzyć do dalszego przetworzenia. W tej sekcji znajdziesz przykłady otwierania bitmapy i strumienia wejściowego.
Bitmapa
Ten fragment kodu pokazuje, jak otworzyć plik Bitmap
z podanym identyfikatorem 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 bitmapy możesz ją wyświetlić na mapie ImageView
.
Strumień wejściowy
Ten fragment kodu pokazuje, jak otworzyć obiekt IngressStream z jego identyfikatorem URI. W tym fragmencie wiersze pliku są odczytywane w postaci 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
Dokument tekstowy możesz też edytować za pomocą platformy Storage Access Framework.
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 Document.COLUMN_FLAGS
zawiera SUPPORTS_DELETE
, możesz usunąć dokument. Na przykład:
Kotlin
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
Java
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);
Pobierz równoważny identyfikator URI multimediów
Metoda getMediaUri()
udostępnia identyfikator URI magazynu multimediów, który jest odpowiednikiem danego identyfikatora URI dostawcy dokumentów. Oba identyfikatory URI odnoszą się do tego samego elementu podstawowego. 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
. W Androidzie 12 (poziom interfejsu API 31) i nowszych metoda obsługuje też identyfikatory URI MediaDocumentsProvider
.
Otwieranie pliku wirtualnego
W Androidzie 7.0 (poziom interfejsu API 25) i nowszych aplikacja może korzystać z plików wirtualnych udostępnianych przez Storage Access Framework. Mimo że pliki wirtualne nie mają reprezentacji binarnej, aplikacja może otworzyć ich zawartość przez zmuszenie ich do zmiany typu pliku lub wyświetlenie tych plików za pomocą działania intencji ACTION_VIEW
.
Aby można było otwierać pliki wirtualne, aplikacja kliencka musi zawierać specjalną logikę do ich obsługi. Jeśli chcesz, by plik był przedstawiany w bajtach (na przykład na potrzeby jego podglądu), musisz poprosić dostawcę dokumentów o alternatywny typ MIME.
Gdy użytkownik dokona wyboru, użyj w danych wyników identyfikatora URI, 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 zmienić go na inny typ MIME, na przykład "image/png"
. Poniższy fragment kodu pokazuje, jak sprawdzić, czy plik wirtualny może być reprezentowany jako obraz, a jeśli tak, pobiera 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 plikach oraz uzyskiwanie do nich dostępu znajdziesz w tych materiałach:
Próbki
- Plik ActionOpenDocument dostępny na GitHubie.
- Plik ActionOpenDocumentTree dostępny na GitHubie.