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 woluminy pamięci zewnętrznej i miejsca w chmurze, z użyciem funkcji Platforma dostępu. Ta platforma umożliwia użytkownikom korzystanie z systemowego selektora do wybierania dostawcy dokumentów i określania konkretnych dokumentów oraz innych plików, które aplikacja ma tworzyć, otwierać i modyfikować.
Ponieważ użytkownik bierze udział w wyborze plików lub katalogów aplikacji nie może uzyskać dostępu, jednak ten mechanizm nie wymaga żadnego systemu uprawnień oraz kontroli i prywatności użytkowników jest ulepszony. Dodatkowo pliki te są przechowywane poza w katalogu aplikacji i poza sklepem multimedialnym, pozostaną 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 platformy i dostępności informacji.
- Użytkownik widzi selektor systemowy, który umożliwia przeglądanie dostawcy dokumentów i wybranie lokalizacji lub dokumentu, w przypadku których ma zostać wykonane działanie związane z przechowywaniem.
- Aplikacja uzyskuje dostęp do odczytu i zapisu do identyfikatora URI, który reprezentuje wybraną przez użytkownika lokalizację lub dokument. Za pomocą tego identyfikatora URI aplikacja może wykonywać operacje na w wybranej lokalizacji.
Aby umożliwić dostęp do plików multimedialnych na urządzeniach z Androidem 9 (poziom interfejsu API 28) lub niższym, zadeklaruj uprawnienie READ_EXTERNAL_STORAGE
i ustaw wartość parametru maxSdkVersion
na 28
.
W tym przewodniku objaśniamy różne przypadki użycia obsługiwane przez platformę pracy z plikami i innymi dokumentami. Podaje też, jak osiągnąć na wybranej przez użytkownika lokalizacji.
Przypadki użycia dostępu do dokumentów i innych plików
Platforma Storage Access Framework obsługuje te przypadki użycia dotyczące dostępu do plików i innych dokumentów.
- Tworzenie nowego pliku
- Akcja intencjonalna
ACTION_CREATE_DOCUMENT
pozwala użytkownikom zapisać plik w konkretnej lokalizacji. - Otwieranie dokumentu lub pliku
-
ACTION_OPEN_DOCUMENT
. działanie intencji pozwala użytkownikom wybrać konkretny dokument lub plik do otwarcia. - Przyznawanie dostępu do zawartości katalogu
- Działanie intencyjne
ACTION_OPEN_DOCUMENT_TREE
, dostępne w Androidzie 5.0 (poziom API 21) i nowszych wersjach, umożliwia użytkownikom wybranie konkretnego katalogu, co daje Twojej aplikacji dostęp do wszystkich plików i podkatalogów w tym katalogu.
W poniższych sekcjach znajdziesz wskazówki dotyczące konfigurowania poszczególnych przypadków użycia.
Tworzenie nowego pliku
Użyj działania intencyjnego ACTION_CREATE_DOCUMENT
, aby załadować okno wyboru plików systemu i zezwolić użytkownikowi na wybranie lokalizacji, w której ma zostać zapisany plik. Ten proces jest podobny do
jeden używany w funkcji „zapisz jako” okien dialogowych używanych w innych systemach operacyjnych.
Uwaga: ACTION_CREATE_DOCUMENT
nie może zastąpić
już istniejący plik. Jeśli aplikacja próbuje zapisać plik o tej samej nazwie, system doda na końcu nazwy pliku liczbę w nawiasach.
Jeśli na przykład aplikacja spróbuje zapisać plik o nazwie confirmation.pdf
w katalogu, w którym znajduje się już plik o tej nazwie, system zapisze nowy plik pod nazwą confirmation(1).pdf
.
Podczas konfigurowania intencji określ nazwę pliku i typ MIME.
opcjonalnie podaj identyfikator URI pliku lub katalogu, który selektor plików
można wyświetlić przy pierwszym wczytaniu, używając
EXTRA_INITIAL_URI
intencja użytkownika.
Fragment kodu poniżej pokazuje, jak utworzyć i wywołać intencję dla utworzenie pliku:
// 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) }
// 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 zapisują dane aby udostępnić je innym lub zaimportować do innych dokumentów. Kilka Przykłady to m.in. użytkownik, który otwiera dokument dotyczący produktywności lub książkę zapisane jako plik EPUB.
W takich przypadkach pozwól użytkownikowi wybrać plik do otwarcia, wywołując metodę
ACTION_OPEN_DOCUMENT
który otwiera systemową aplikację z selektorem plików. Aby wyświetlić tylko typy
obsługiwanych przez aplikację, wybierz typ MIME. Opcjonalnie możesz też
określ identyfikator URI pliku, który selektor plików ma wyświetlić po pierwszym otwarciu
ładuje się za pomocą
EXTRA_INITIAL_URI
intencja użytkownika.
Ten fragment kodu pokazuje, jak utworzyć i wywołać intencję otwierania dokumentu PDF:
// 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) }
// 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 interfejsu API 30) i nowszym nie można używać funkcji
ACTION_OPEN_DOCUMENT
działanie intencji, które prosi użytkownika o wybranie innej osoby
w następujących katalogach:
- 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
hierarchii katalogów. Aby umożliwić tę funkcję w aplikacji, użyj działania intencjonalnego ACTION_OPEN_DOCUMENT_TREE
, które pozwala użytkownikowi przyznać dostęp do całego drzewa katalogów, z pewnymi wyjątkami od Androida 11 (poziom interfejsu API 30). Aplikacja może wtedy uzyskać dostęp do dowolnego pliku 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 innych aplikacji
aplikacji plików znajdujących się poza tym katalogiem wybranym przez użytkownika. Ten
dostęp kontrolowany przez użytkowników pozwala użytkownikom dokładnie wybrać, jakie treści chcą oglądać
do udostępniania swojej aplikacji.
Opcjonalnie możesz określić URI katalogu, który selektor plików powinien wyświetlić podczas pierwszego wczytania, używając opcjonalnego atrybutu intencji EXTRA_INITIAL_URI
.
Ten fragment kodu pokazuje, jak utworzyć i wywołać intencję otwierania katalogu:
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 ) }
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żna używać funkcji
ACTION_OPEN_DOCUMENT_TREE
działanie intencji, które prosi o dostęp do tych elementów
katalogi:
- Katalog główny woluminu pamięci wewnętrznej.
- Katalog główny każdej karty SD, którą producent urządzenia uznaje za wiarygodną, niezależnie od tego, czy karta jest emulowana, czy wymienna. Niezawodny wolumin to taki, do którego aplikacja ma dostęp do większości w określonym czasie.
- Katalog
Download
.
Ponadto w Androidzie 11 (poziom interfejsu API 30) i nowszych nie możesz używać działania intencjonalnego 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 w systemowym selektorze plików,
możesz pobrać identyfikator URI wybranego elementu przy użyciu poniższego kodu w sekcji
onActivityResult()
:
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. } } }
@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 odniesieniu do identyfikatora URI wybranego elementu aplikacja może wykonywać kilka na elemencie. Możesz na przykład uzyskać dostęp do metadanych elementu, edytować element w danym miejscu, a następnie go usuń.
W kolejnych sekcjach dowiesz się, jak wykonywać działania na plikach wybranych przez użytkownika.
Określ operacje obsługiwane 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ść parametru Document.COLUMN_FLAGS
.
Interfejs aplikacji może wtedy wyświetlać tylko opcje obsługiwane przez dostawcę.
Zachowaj uprawnienia
Gdy aplikacja otworzy plik do odczytu lub zapisu, system udostępni jej Uprawnienia do identyfikatora URI przyznane do tego pliku (do momentu, gdy urządzenie użytkownika nie korzysta z tych uprawnień) uruchomi się ponownie. Załóżmy jednak, że Twoja aplikacja służy do edycji zdjęć i chcesz, aby użytkownicy mogli uzyskać dostęp do 5 ostatnio edytowanych zdjęć bezpośrednio z aplikacji. Jeśli urządzenie użytkownika zostanie ponownie uruchomione, będzie musiał wrócić do selektora systemowego, aby znaleźć pliki.
Aby zachować dostęp do plików na różnych urządzeniach po ponownym uruchomieniu i utworzyć lepsze konto użytkownika aplikacja może „przyjąć” trwałe uprawnienie identyfikatora URI przyznaje, jak w poniższym fragmencie kodu:
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)
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 po uzyskaniu identyfikatora URI. Ten pobiera metadane dokumentu wskazanego przez identyfikator URI i zapisuje je w dzienniku:
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") } } }
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. Ta sekcja pokazuje przykłady otwierania bitmapy i danych wejściowych .
Bitmapa
Fragment kodu poniżej pokazuje, jak otworzyć plik
Bitmap
z podanym identyfikatorem URI:
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 }
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 wyświetlić ją w oknie ImageView
.
Strumień wejściowy
Z tego fragmentu kodu dowiesz się, jak otworzyć obiekt IngressStream, któremu Identyfikator URI. W tym fragmencie wiersze pliku są odczytywane w postaci ciągu znaków:
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() }
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 dany identyfikator URI:
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() } }
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 znasz identyfikator URI dokumentu oraz
Document.COLUMN_FLAGS
zawiera
SUPPORTS_DELETE
,
możesz usunąć ten dokument. Na przykład:
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);
Pobierz równoważny identyfikator URI multimediów
getMediaUri()
udostępnia identyfikator URI magazynu multimediów, który jest równoważny z podanymi dokumentami
identyfikatora URI dostawcy. Oba identyfikatory URI odnoszą się do tego samego elementu. Za pomocą adresu 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 ta metoda obsługuje też URI MediaDocumentsProvider
.
Otwieranie pliku wirtualnego
W Androidzie 7.0 (poziom interfejsu API 25) i nowszym 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ść, wymuszając na nich inny typ pliku lub wyświetlając je za pomocą działania intencyjnego ACTION_VIEW
.
Aby otwierać pliki wirtualne, aplikacja klienta musi zawierać specjalną logikę do ich obsługi. Jeśli chcesz uzyskać reprezentację bajtową pliku (np. aby wyświetlić jego podgląd), 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ć w przypadku plików wirtualnych, zgodnie z tym fragmentem kodu:
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 }
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 potwierdzeniu, że dokument jest plikiem wirtualnym, możesz wymusić
do innego typu MIME, np. "image/png"
. Poniższy fragment kodu pokazuje, jak sprawdzić, czy plik wirtualny może być reprezentowany jako obraz, a jeśli tak, pobiera strumień wejściowy z pliku wirtualnego:
@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() } }
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 zapoznaj się z tymi materiałami.
Próbki
- ActionOpenDocument, znajdziesz na GitHubie.
- ActionOpenDocumentTree, dostępna w GitHub.