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 interakcję z selektorem systemu aby wybrać dostawcę dokumentów i określone dokumenty oraz inne pliki w aplikacji, aby ją utworzyć, otworzyć lub zmodyfikować.
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 wybierz lokalizację lub dokument, w którym ma nastąpić działanie związane z przechowywaniem.
- Aplikacja uzyskuje uprawnienia do odczytu i zapisu do identyfikatora URI reprezentującego wybranej lokalizacji lub dokumentu. Za pomocą tego identyfikatora URI aplikacja może wykonywać operacje na w wybranej lokalizacji.
aby obsługiwać dostęp do plików multimedialnych na urządzeniach z Androidem 9 (poziom interfejsu API 28) lub
poniżej, zadeklaruj
READ_EXTERNAL_STORAGE
i ustaw 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 dostępu do pamięci masowej obsługuje poniższe przypadki użycia pliki i inne dokumenty.
- Tworzenie nowego pliku
-
ACTION_CREATE_DOCUMENT
. działanie intencji pozwala użytkownikom zapisać plik w określonej 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
-
ACTION_OPEN_DOCUMENT_TREE
. działanie intencji, dostępne na Androidzie 5.0 (poziom interfejsu API 21) i nowszym, pozwala użytkownikom aby wybrać konkretny katalog i przyznać aplikacji dostęp do wszystkich podkatalogów w danym katalogu.
W sekcjach poniżej znajdziesz wskazówki dotyczące konfigurowania poszczególnych przypadków użycia.
Tworzenie nowego pliku
Użyj
ACTION_CREATE_DOCUMENT
działanie intencji, które wczytuje systemowy selektor plików i umożliwia użytkownikowi wybranie
lokalizację, w której można zapisać zawartość pliku. 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 spróbuje zapisać plik o tej samej nazwie, system
dodaje liczbę w nawiasach na końcu nazwy pliku.
Jeśli na przykład aplikacja próbuje zapisać plik o nazwie
confirmation.pdf
jest w katalogu, który ma już plik z tym elementem
system zapisze nowy plik o nazwie
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:
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 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.
Fragment kodu poniżej pokazuje, jak utworzyć i wywołać intencję otwarcia dokument 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 interfejsu API 30) i nowszym nie można używać funkcji
ACTION_OPEN_DOCUMENT
działanie zamiarowe, aby użytkownik wybrał inną osobę
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 udostępnić tę funkcję w aplikacji, skorzystaj z
ACTION_OPEN_DOCUMENT_TREE
działanie intencji, które umożliwia użytkownikowi przyznanie dostępu do całego katalogu
drzewo z kilkoma wyjątkami od Androida 11 (poziom interfejsu API 30). Aplikacja może
a następnie uzyskać dostęp do dowolnego pliku w wybranym katalogu i jego podkatalogach.
Gdy używasz funkcji ACTION_OPEN_DOCUMENT_TREE
, aplikacja uzyska dostęp tylko do
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ć
udostępnianie swojej aplikacji.
Opcjonalnie możesz podać identyfikator URI katalogu, który powinien być używany przez 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ę otwarcia katalog:
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ż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 woluminu karty SD używanego przez producenta urządzenia uważany jest za niezawodny, niezależnie od tego, czy karta jest emulowana, lub ich usunięcia. 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żna używać funkcji
ACTION_OPEN_DOCUMENT_TREE
działanie intencji, które prosi użytkownika o wybór
z 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()
:
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 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 poniższych sekcjach znajdziesz informacje o tym, jak wykonywać działania na plikach, które użytkownik wybierze te opcje.
Określ operacje obsługiwane przez dostawcę
Różni dostawcy treści pozwalają na wykonywanie odmiennych działań
dokumentów, na przykład przez skopiowanie dokumentu lub wyświetlenie jego miniatury. Do
Określanie operacji obsługiwanych przez danego dostawcę, sprawdzanie wartości
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 korzysta z tego identyfikatora); uruchomi się ponownie. Załóżmy jednak, że Twoja aplikacja jest przeznaczona do edycji obrazów i chcesz użytkownicy mają dostęp do 5 ostatnio edytowanych obrazów bezpośrednio z aplikacji. Jeśli urządzenie użytkownika zostało ponownie uruchomione, musisz wysłać do niego informacje wróć 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:
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 pobiera metadane dokumentu określonego przez identyfikator URI i zapisuje je w dzienniku:
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
Dokument możesz otworzyć, korzystając z odniesienia do jego identyfikatora URI o przetwarzaniu danych. 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:
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 wyświetlić ją w
ImageView
Strumień wejściowy
Poniższy fragment kodu pokazuje, jak otworzyć obiekt w usłudzeinputStream z uwzględnieniem jego argumentu Identyfikator 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 treść reprezentowanego dokumentu według danego identyfikatora 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 znasz identyfikator URI dokumentu oraz
Document.COLUMN_FLAGS
zawiera
SUPPORTS_DELETE
,
możesz usunąć ten dokument. Na przykład:
Kotlin
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
Java
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 podstawowego. Korzystanie z multimediów
identyfikatora URI sklepu, łatwiej dostęp do plików multimedialnych z
miejsca na dane.
Metoda getMediaUri()
obsługuje identyfikatory URI ExternalStorageProvider
. Wł.
Androida 12 (poziom interfejsu API 31) lub nowszego; 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
które udostępnia platforma Storage Access Framework. Mimo że pliki wirtualne
nie ma reprezentacji binarnej, aplikacja może otworzyć swoją zawartość przez przymus
ich do innego typu lub wyświetlając je za pomocą
Zamiar ACTION_VIEW
działania.
Aby można było otwierać pliki wirtualne, aplikacja kliencka musi zawierać specjalną logikę do obsługi . Jeśli chcesz zapisać plik w postaci bajtów (podgląd pliku), na przykład – musisz poprosić o alternatywny typ MIME z dokumentów. dostawcy usług.
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:
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 potwierdzeniu, że dokument jest plikiem wirtualnym, możesz zmienić metodę
do innego typu MIME, np. "image/png"
. Następujący kod:
pokazuje, jak sprawdzić, czy plik wirtualny może być reprezentowany
obrazu. Jeśli tak, pobierany jest strumień danych wejściowych z pliku wirtualnego:
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 zapoznaj się z tymi materiałami.
Próbki
- ActionOpenDocument, znajdziesz na GitHubie.
- ActionOpenDocumentTree, znajdziesz na GitHubie.