| page.title=Платформа доступа к хранилищу (Storage Access Framework) |
| @jd:body |
| <div id="qv-wrapper"> |
| <div id="qv"> |
| |
| <h2>Содержание документа |
| <a href="#" onclick="hideNestedItems('#toc44',this);return false;" class="header-toggle"> |
| <span class="more">больше информации</span> |
| <span class="less" style="display:none">меньше информации</span></a></h2> |
| <ol id="toc44" class="hide-nested"> |
| <li> |
| <a href="#overview">Обзор</a> |
| </li> |
| <li> |
| <a href="#flow">Поток управления</a> |
| </li> |
| <li> |
| <a href="#client">Создание клиентского приложения</a> |
| <ol> |
| <li><a href="#search">Поиск документов</a></li> |
| <li><a href="#process">Обработка результатов</a></li> |
| <li><a href="#metadata">Изучение метаданных документа</a></li> |
| <li><a href="#open">Открытие документа</a></li> |
| <li><a href="#create">Создание нового документа</a></li> |
| <li><a href="#delete">Удаление документа</a></li> |
| <li><a href="#edit">Редактирование документа</a></li> |
| <li><a href="#permissions">Удержание прав доступа</a></li> |
| </ol> |
| </li> |
| <li><a href="#custom">Создание собственного поставщика документов</a> |
| <ol> |
| <li><a href="#manifest">Манифест</a></li> |
| <li><a href="#contract">Контракты</a></li> |
| <li><a href="#subclass">Создание подкласса класса DocumentsProvider</a></li> |
| <li><a href="#security">Безопасность</a></li> |
| </ol> |
| </li> |
| |
| </ol> |
| <h2>Ключевые классы</h2> |
| <ol> |
| <li>{@link android.provider.DocumentsProvider}</li> |
| <li>{@link android.provider.DocumentsContract}</li> |
| </ol> |
| |
| <h2>Видео</h2> |
| |
| <ol> |
| <li><a href="http://www.youtube.com/watch?v=zxHVeXbK1P4"> |
| DevBytes: Android 4.4 Storage Access Framework: Поставщик</a></li> |
| <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ"> |
| DevBytes: Android 4.4 Storage Access Framework: Клиент</a></li> |
| </ol> |
| |
| |
| <h2>Примеры кода</h2> |
| |
| <ol> |
| <li><a href="{@docRoot}samples/StorageProvider/index.html"> |
| Класс StorageProvider</a></li> |
| <li><a href="{@docRoot}samples/StorageClient/index.html"> |
| Класс StorageClient</a></li> |
| </ol> |
| |
| <h2>См. также:</h2> |
| <ol> |
| <li> |
| <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> |
| Основные сведения о поставщике контента |
| </a> |
| </li> |
| </ol> |
| |
| </div> |
| </div> |
| |
| |
| <p>Платформа доступа к хранилищу (Storage Access Framework, SAF) впервые появилась в Android версии 4.4 (API уровня 19). Платформа SAF |
| облегчает пользователям поиск и открытие документов, изображений и других файлов |
| в хранилищах всех поставщиков, с которыми они работают. Стандартный удобный интерфейс |
| позволяет пользователям применять единый для всех приложений и поставщиков способ поиска файлов и доступа к последним добавленным файлам.</p> |
| |
| <p>Облачные или локальные службы хранения могут присоединиться к этой экосистеме, реализовав |
| класс {@link android.provider.DocumentsProvider}, инкапсулирующий их услуги. Клиентские |
| приложения, которым требуется доступ к документам поставщика, могут интегрироваться с SAF с помощью всего нескольких |
| строчек кода.</p> |
| |
| <p>Платформа SAF включает в себя следующие компоненты:</p> |
| |
| <ul> |
| <li><strong>Поставщик документов</strong>—поставщик контента, позволяющий |
| службе хранения (например, Диск Google) показывать файлы, которыми он управляет. Поставщик документов |
| реализуется как подкласс класса{@link android.provider.DocumentsProvider}. |
| Его схема основана на традиционной файловой иерархии, |
| однако физический способ хранения данных в поставщике документов остается на усмотрении разработчика. |
| Платформа Android включает в себя несколько встроенных поставщиков документов, таких как |
| Загрузки, Изображения и Видео.</li> |
| |
| <li><strong>Клиентское приложение</strong>—пользовательское приложение, вызывающее намерение |
| {@link android.content.Intent#ACTION_OPEN_DOCUMENT} и/или |
| {@link android.content.Intent#ACTION_CREATE_DOCUMENT} и принимающее |
| файлы, возвращаемые поставщиками документов.</li> |
| |
| <li><strong>Элемент выбора</strong>—системный пользовательский интерфейс, обеспечивающий пользователям доступ к документам у всех |
| поставщиков документов, которые удовлетворяют критериям поиска, заданным в клиентском приложении.</li> |
| </ul> |
| |
| <p>Платформа SAF в числе прочих предоставляет следующие функции:</p> |
| <ul> |
| <li>позволяет пользователям искать контент у всех поставщиков документов, а не только у одного приложения;</li> |
| <li>обеспечивает приложению возможность долговременного, постоянного доступа к |
| документам, принадлежащим поставщику документов. Благодаря такому доступу пользователи могут добавлять, редактировать, |
| сохранять и удалять файлы, хранящиеся у поставщика;</li> |
| <li>поддерживает несколько учетных записей и временные корневые каталоги, например, поставщики |
| на USB-накопителях, которые появляются, только когда накопитель вставлен в порт. </li> |
| </ul> |
| |
| <h2 id ="overview">Обзор</h2> |
| |
| <p>В центре платформы SAF находится поставщик контента, являющийся |
| подклассом класса {@link android.provider.DocumentsProvider}. Внутри <em>поставщика документов</em>данные имеют |
| структуру традиционной файловой иерархии:</p> |
| <p><img src="{@docRoot}images/providers/storage_datamodel.png" alt="data model" /></p> |
| <p class="img-caption"><strong>Рисунок 1.</strong> Модель данных поставщика документов. На рисунке Root (Корневой каталог) указывает на один объект Document (Документ), |
| который затем разветвляется в целое дерево.</p> |
| |
| <p>Обратите внимание на следующее.</p> |
| <ul> |
| |
| <li>Каждый поставщик документов предоставляет один или несколько |
| «корневых каталогов», являющихся отправными точками при обходе дерева документов. |
| Каждый корневой каталог имеет уникальный идентификатор {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID} |
| и указывает на документ (каталог), |
| представляющий содержимое на уровне ниже корневого. |
| Корневые каталоги динамичны по своей конструкции, чтобы обеспечивать поддержку таким вариантам использования, как несколько учетных записей, |
| временные хранилища на USB-нкопителях и возможность для пользователя войти в систему и выйти из нее.</li> |
| |
| <li>В каждом корневом каталоге находится один документ. Этот документ указывает на количество документов <em>N</em> |
| каждый из которых, в свою очередь, может указывать на один или <em>N</em> документов. </li> |
| |
| <li>Каждый сервер хранилища показывает |
| отдельные файлы и каталоги, ссылаясь на них с помощью уникального |
| идентификатора {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID}. |
| Идентификаторы документов должны быть уникальными и не меняться после присвоения, поскольку они используются для выдачи постоянных |
| URI, не зависящих от перезагрузки устройства.</li> |
| |
| |
| <li>Документ — это или открываемый файл (имеющий конкретный MIME-тип), или |
| каталог, содержащий другие документы (с |
| MIME-типом {@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR}).</li> |
| |
| <li>Каждый документ может иметь различные свойства, описываемые флагами |
| {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS}, |
| такими как{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE}, |
| {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE} и |
| {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL}. |
| Документ с одним и тем же идентификатором {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} может находиться |
| в нескольких каталогах.</li> |
| </ul> |
| |
| <h2 id="flow">Поток управления</h2> |
| <p>Как было сказано выше, модель данных поставщика документов основана на традиционной |
| файловой иерархии. Однако физический способ хранения данных остается на усмотрение разработчика, при |
| условии, что к ним можно обращаться через API-интерфейс {@link android.provider.DocumentsProvider}. Например, можно |
| использовать для данных облачное хранилище на основе тегов.</p> |
| |
| <p>На рисунке 2 показан пример того, как приложение для обработки фотографий может использовать SAF |
| для доступа к сохраненным данным:</p> |
| <p><img src="{@docRoot}images/providers/storage_dataflow.png" alt="app" /></p> |
| |
| <p class="img-caption"><strong>Рисунок 2.</strong> Поток управления Storage Access Framework</p> |
| |
| <p>Обратите внимание на следующее.</p> |
| <ul> |
| |
| <li>На платформе SAF поставщики и клиенты не взаимодействуют |
| напрямую. Клиент запрашивает разрешение на взаимодействие |
| с файлами (то есть, на чтение, редактирование, создание или удаление файлов).</li> |
| |
| <li>Взаимодействие начинается, когда приложение (в нашем примере обрабатывающее фотографии) активизирует намерение |
| {@link android.content.Intent#ACTION_OPEN_DOCUMENT} или {@link android.content.Intent#ACTION_CREATE_DOCUMENT}. Намерение может включать в себя фильтры |
| для уточнения критериев, например, «предоставить открываемые файлы |
| с MIME-типом image».</li> |
| |
| <li>Когда намерение срабатывает, системный элемент выбора переходит к каждому зарегистрированному поставщику |
| и показывает пользователю корневые каталоги с контентом, соответствующим запросу.</li> |
| |
| <li>Элемент выбора предоставляет пользователю стандартный интерфейс, даже |
| если поставщики документов значительно различаются. В качестве примера на рисунке 2 |
| изображены Диск Google, поставщик на USB-накопителе и облачный поставщик.</li> |
| </ul> |
| |
| <p>На рисунке 3 показан элемент выбора, в котором пользователь для поиска изображений выбрал учетную запись |
| Диск Google:</p> |
| |
| <p><img src="{@docRoot}images/providers/storage_picker.png" width="340" alt="picker" style="border:2px solid #ddd" /></p> |
| |
| <p class="img-caption"><strong>Рисунок 3.</strong> Элемент выбора</p> |
| |
| <p>Когда пользователь выбирает Диск Google, изображения отображаются, как показано на |
| рисунке 4. С этого момента пользователь может взаимодействовать с ними любыми способами, |
| которые поддерживаются поставщиком и клиентским приложением. |
| |
| <p><img src="{@docRoot}images/providers/storage_photos.png" width="340" alt="picker" style="border:2px solid #ddd" /></p> |
| |
| <p class="img-caption"><strong>Рисунок 4.</strong> Изображения</p> |
| |
| <h2 id="client">Создание клиентского приложения</h2> |
| |
| <p>В Android версии 4.3 и ниже для того, чтобы приложение могло получать файл от другого |
| приложения, оно должно активизировать намерение, например, {@link android.content.Intent#ACTION_PICK} |
| или {@link android.content.Intent#ACTION_GET_CONTENT}. После этого пользователь должен выбрать |
| какое-либо одно приложение, чтобы получить файл, а оно должно предоставить пользователю |
| интерфейс, с помощью которого он сможет выбирать и получать файлы. </p> |
| |
| <p>Начиная с Android 4.4 и выше, у разработчика имеется дополнительная возможность — намерение |
| {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, |
| которое отображает пользовательский интерфейс элемента выбора, управляемого системой. Этот элемент предоставляет пользователю |
| обзор всех файлов, доступных в других приложениях. Благодаря этому единому интерфейсу, |
| пользователь может выбрать файл в любом из поддерживаемых приложений.</p> |
| |
| <p>Намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT} не |
| является заменой для намерения {@link android.content.Intent#ACTION_GET_CONTENT}. |
| Разработчику следует использовать то, которое лучше соответствует потребностям приложения:</p> |
| |
| <ul> |
| <li>используйте {@link android.content.Intent#ACTION_GET_CONTENT}, если приложению нужно просто |
| прочитать или импортировать данные. При таком подходе приложение импортирует копию данных, |
| например, файл с изображением.</li> |
| |
| <li>используйте {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, если |
| приложению нужна возможность долговременного, постоянного доступа к документам, принадлежащим поставщику |
| документов. В качестве примера можно назвать редактор фотографий, позволяющий пользователям обрабатывать |
| изображения, хранящиеся в поставщике документов. </li> |
| |
| </ul> |
| |
| |
| <p>В этом разделе показано, как написать клиентское приложение, использующее намерения |
| {@link android.content.Intent#ACTION_OPEN_DOCUMENT} и |
| {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.</p> |
| |
| |
| <h3 id="search">Поиск документов</h3> |
| |
| <p> |
| В следующем фрагменте кода намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT} |
| используется для поиска поставщиков документов, |
| содержащих файлы изображений:</p> |
| |
| <pre>private static final int READ_REQUEST_CODE = 42; |
| ... |
| /** |
| * Fires an intent to spin up the "file chooser" UI and select an image. |
| */ |
| public void performFileSearch() { |
| |
| // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file |
| // browser. |
| Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); |
| |
| // Filter to only show results that can be "opened", such as a |
| // file (as opposed to a list of contacts or timezones) |
| intent.addCategory(Intent.CATEGORY_OPENABLE); |
| |
| // Filter to show only images, using the image MIME data type. |
| // If one wanted to search for ogg vorbis files, the type would be "audio/ogg". |
| // To search for all documents available via installed storage providers, |
| // it would be "*/*". |
| intent.setType("image/*"); |
| |
| startActivityForResult(intent, READ_REQUEST_CODE); |
| }</pre> |
| |
| <p>Обратите внимание на следующее.</p> |
| <ul> |
| <li>Когда приложение активизирует намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT} |
| , оно запускает элемент выбора, отображающий всех поставщиков документов, соответствующих заданным критериям.</li> |
| |
| <li>Добавление категории {@link android.content.Intent#CATEGORY_OPENABLE} в |
| фильтры намерения приводит к отображению только тех документов, которые можно открыть, например, файлов с изображениями.</li> |
| |
| <li>Оператор {@code intent.setType("image/*")} выполняет дальнейшую фильтрацию, чтобы |
| отображались только документы с MIME-типом image.</li> |
| </ul> |
| |
| <h3 id="results">Обработка результатов</h3> |
| |
| <p>Когда пользователь выбирает документ в элементе выбора, |
| вызывается метод {@link android.app.Activity#onActivityResult onActivityResult()}. |
| Идентификатор URI, указывающий на выбранный документ, содержится в параметре{@code resultData}. |
| Чтобы извлечь URI, следует вызвать {@link android.content.Intent#getData getData()}. |
| Этот URI можно использовать для получения документа, нужного пользователю. Например: |
| </p> |
| |
| <pre>@Override |
| public void onActivityResult(int requestCode, int resultCode, |
| Intent resultData) { |
| |
| // The ACTION_OPEN_DOCUMENT intent was sent with the request code |
| // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the |
| // response to some other intent, and the code below shouldn't run at all. |
| |
| if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { |
| // The document selected by the user won't be returned in the intent. |
| // Instead, a URI to that document will be contained in the return intent |
| // provided to this method as a parameter. |
| // Pull that URI using resultData.getData(). |
| Uri uri = null; |
| if (resultData != null) { |
| uri = resultData.getData(); |
| Log.i(TAG, "Uri: " + uri.toString()); |
| showImage(uri); |
| } |
| } |
| } |
| </pre> |
| |
| <h3 id="metadata">Изучение метаданных документа</h3> |
| |
| <p>Имея в своем распоряжении URI документа, разработчик получает доступ к его метаданным. В следующем |
| фрагменте кода метаданные документа, определяемого идентификатором URI, считываются и записываются в журнал:</p> |
| |
| <pre>public void dumpImageMetaData(Uri uri) { |
| |
| // The query, since it only applies to a single document, will only return |
| // one row. There's no need to filter, sort, or select fields, since 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 since an |
| // int can't be null in Java, the behavior is implementation-specific, |
| // which is just a fancy term for "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(); |
| } |
| } |
| </pre> |
| |
| <h3 id="open-client">Открытие документа</h3> |
| |
| <p>Получив URI документа, разработчик может открывать его и в целом |
| делать с ним всё, что угодно.</p> |
| |
| <h4>Объект растровых изображений</h4> |
| |
| <p>Приведем пример кода для открытия объекта {@link android.graphics.Bitmap}:</p> |
| |
| <pre>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; |
| } |
| </pre> |
| |
| <p>Обратите внимание, что не следует производить эту операцию в потоке пользовательского интерфейса. Ее нужно выполнять |
| в фоне, с помощью {@link android.os.AsyncTask}. Когда файл с растровым изображением откроется, его |
| можно отобразить в виджете {@link android.widget.ImageView}. |
| </p> |
| |
| <h4>Получение объекта InputStream</h4> |
| |
| <p>Далее приведен пример того, как можно получить объект {@link java.io.InputStream} по идентификатору URI. В этом |
| фрагменте кода строчки файла считываются в объект строкового типа:</p> |
| |
| <pre>private String readTextFromUri(Uri uri) throws IOException { |
| InputStream inputStream = getContentResolver().openInputStream(uri); |
| BufferedReader reader = new BufferedReader(new InputStreamReader( |
| inputStream)); |
| StringBuilder stringBuilder = new StringBuilder(); |
| String line; |
| while ((line = reader.readLine()) != null) { |
| stringBuilder.append(line); |
| } |
| fileInputStream.close(); |
| parcelFileDescriptor.close(); |
| return stringBuilder.toString(); |
| } |
| </pre> |
| |
| <h3 id="create">Создание нового документа</h3> |
| |
| <p>Приложение может создать новый документ в поставщике документов, используя намерение |
| {@link android.content.Intent#ACTION_CREATE_DOCUMENT} |
| . Чтобы создать файл, нужно указать в намерении MIME-тип и имя файла, а затем |
| запустить его с уникальным кодом запроса. Об остальном позаботится платформа:</p> |
| |
| |
| <pre> |
| // Here are some examples of how you might call this method. |
| // The first parameter is the MIME type, and the second parameter is the name |
| // of the file you are creating: |
| // |
| // createFile("text/plain", "foobar.txt"); |
| // createFile("image/png", "mypicture.png"); |
| |
| // Unique request code. |
| private static final int WRITE_REQUEST_CODE = 43; |
| ... |
| private void createFile(String mimeType, String fileName) { |
| Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); |
| |
| // Filter to only show results that can be "opened", such as |
| // a file (as opposed to a list of contacts or timezones). |
| intent.addCategory(Intent.CATEGORY_OPENABLE); |
| |
| // Create a file with the requested MIME type. |
| intent.setType(mimeType); |
| intent.putExtra(Intent.EXTRA_TITLE, fileName); |
| startActivityForResult(intent, WRITE_REQUEST_CODE); |
| } |
| </pre> |
| |
| <p>После создания нового документа можно получить его URI с помощью |
| метода {@link android.app.Activity#onActivityResult onActivityResult()}, чтобы иметь возможность |
| записывать в него данные.</p> |
| |
| <h3 id="delete">Удаление документа</h3> |
| |
| <p>Если у разработчика имеется URI документа, а объект |
| {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} |
| этого документа содержит флаг |
| {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}, |
| то документ можно удалить. Например:</p> |
| |
| <pre> |
| DocumentsContract.deleteDocument(getContentResolver(), uri); |
| </pre> |
| |
| <h3 id="edit">Редактирование документа</h3> |
| |
| <p>Платформа SAF позволяет редактировать текстовые документы на месте. |
| В следующем фрагменте кода активизируется |
| намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, а |
| категория {@link android.content.Intent#CATEGORY_OPENABLE} используется, чтобы отображались только |
| документы, которые можно открыть. Затем производится дальнейшая фильтрация, чтобы отображались только текстовые файлы:</p> |
| |
| <pre> |
| private static final int EDIT_REQUEST_CODE = 44; |
| /** |
| * Open a file for writing and append some text to it. |
| */ |
| private void editDocument() { |
| // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's |
| // file browser. |
| Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); |
| |
| // Filter to only show results that can be "opened", such as a |
| // file (as opposed to a list of contacts or timezones). |
| intent.addCategory(Intent.CATEGORY_OPENABLE); |
| |
| // Filter to show only text files. |
| intent.setType("text/plain"); |
| |
| startActivityForResult(intent, EDIT_REQUEST_CODE); |
| } |
| </pre> |
| |
| <p>Далее, из метода {@link android.app.Activity#onActivityResult onActivityResult()} |
| (см. <a href="#results">Обработка результатов</a>) можно вызвать код для выполнения редактирования. |
| В следующем фрагменте кода объект {@link java.io.FileOutputStream} |
| получен с помощью объекта класса {@link android.content.ContentResolver}. По умолчанию используется режим записи. |
| Рекомендуется запрашивать минимально необходимые права доступа, поэтому не следует запрашивать |
| чтение/запись, если приложению требуется только записать файл:</p> |
| |
| <pre>private void alterDocument(Uri uri) { |
| try { |
| ParcelFileDescriptor pfd = getActivity().getContentResolver(). |
| openFileDescriptor(uri, "w"); |
| FileOutputStream fileOutputStream = |
| new FileOutputStream(pfd.getFileDescriptor()); |
| fileOutputStream.write(("Overwritten by MyCloud 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(); |
| } |
| }</pre> |
| |
| <h3 id="permissions">Удержание прав доступа</h3> |
| |
| <p>Когда приложение открывает файл для чтения или записи, система предоставляет |
| ему URI-разрешение на этот файл. Разрешение действует вплоть до перезагрузки устройства. |
| Предположим, что в графическом редакторе требуется, чтобы у пользователя была возможность |
| открыть непосредственно в этом приложении последние пять изображений, которые он редактировал. Если он |
| перезапустил устройство, возникает необходимость снова отсылать его к системному элементу выбора для поиска |
| файлов. Очевидно, это далеко не идеальный вариант.</p> |
| |
| <p>Чтобы избежать такой ситуации, разработчик может удержать права доступа, предоставленные системой |
| его приложению. Приложение фактически принимает постоянное URI-разрешение, |
| предлагаемое системой. В результате пользователь получает непрерывный доступ к файлам |
| из приложения, независимо от перезагрузки устройства:</p> |
| |
| |
| <pre>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);</pre> |
| |
| <p>Остается один заключительный шаг. Можно сохранить последние |
| URI-идентификаторы, с которыми работало приложение. Однако не исключено, что они потеряют актуальность, поскольку другое приложение |
| может удалить или модифицировать документ. Поэтому следует всегда вызывать |
| {@code getContentResolver().takePersistableUriPermission()}, чтобы получать |
| актуальные данные.</p> |
| |
| <h2 id="custom">Создание собственного поставщика документов</h2> |
| |
| <p> |
| При разработке приложения, оказывающего услуги по хранению файлов (например, |
| службы хранения в облаке), можно предоставить доступ к файлам при помощи |
| SAF, написав собственный поставщик документов. В этом разделе показано, |
| как это сделать.</p> |
| |
| |
| <h3 id="manifest">Манифест</h3> |
| |
| <p>Чтобы реализовать собственный поставщик документов, необходимо добавить в манифест приложения |
| следующую информацию:</p> |
| <ul> |
| |
| <li>Целевой API-интерфейс уровня 19 или выше.</li> |
| |
| <li>Элемент <code><provider></code>, в котором объявляется нестандартный поставщик |
| хранилища. </li> |
| |
| <li>Имя поставщика, т. е., имя его класса с именем пакета. |
| Например: <code>com.example.android.storageprovider.MyCloudProvider</code>.</li> |
| |
| <li>Имя центра поставщика, т. е. имя пакета (в этом примере — |
| <code>com.example.android.storageprovider</code>) с типом поставщика контента |
| (<code>documents</code>). Например,{@code com.example.android.storageprovider.documents}.</li> |
| |
| <li>Атрибут <code>android:exported</code>, установленный в значение <code>"true"</code>. |
| Необходимо экспортировать поставщик, чтобы он был виден другим приложениям.</li> |
| |
| <li>Атрибут <code>android:grantUriPermissions</code>, установленный в значение |
| <code>"true"</code>. Этот параметр позволяет системе предоставлять другим приложениям доступ |
| к контенту поставщика. Обсуждение того, как следует удерживать права доступа |
| к конкретному документу см. в разделе <a href="#permissions">Удержание прав доступа</a>.</li> |
| |
| <li>Разрешение {@code MANAGE_DOCUMENTS}. По умолчанию поставщик доступен |
| всем. Добавление этого разрешения в манифест делает поставщик доступным только системе. |
| Это важно для обеспечения безопасности.</li> |
| |
| <li>Атрибут {@code android:enabled}, имеющий логическое значение, определенное в файле |
| ресурсов. Этот атрибут предназначен для отключения поставщика на устройствах под управлением Android версии 4.3 и ниже. |
| Например: {@code android:enabled="@bool/atLeastKitKat"}. Помимо |
| включения этого атрибута в манифест, необходимо сделать следующее: |
| <ul> |
| <li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values/}, добавить |
| строчку <pre><bool name="atLeastKitKat">false</bool></pre></li> |
| |
| <li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить |
| строчку <pre><bool name="atLeastKitKat">true</bool></pre></li> |
| </ul></li> |
| |
| <li>Фильтр намерения с действием |
| {@code android.content.action.DOCUMENTS_PROVIDER}, чтобы поставщик |
| появлялся в элементе выбора, когда система будет искать поставщиков.</li> |
| |
| </ul> |
| <p>Ниже приведены отрывки из образца манифеста, включающего в себя поставщик:</p> |
| |
| <pre><manifest... > |
| ... |
| <uses-sdk |
| android:minSdkVersion="19" |
| android:targetSdkVersion="19" /> |
| .... |
| <provider |
| android:name="com.example.android.storageprovider.MyCloudProvider" |
| android:authorities="com.example.android.storageprovider.documents" |
| android:grantUriPermissions="true" |
| android:exported="true" |
| android:permission="android.permission.MANAGE_DOCUMENTS" |
| android:enabled="@bool/atLeastKitKat"> |
| <intent-filter> |
| <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> |
| </intent-filter> |
| </provider> |
| </application> |
| |
| </manifest></pre> |
| |
| <h4 id="43">Поддержка устройств под управлением Android версии 4.3 и ниже</h4> |
| |
| <p>Намерение |
| {@link android.content.Intent#ACTION_OPEN_DOCUMENT} доступно только |
| на устройствах с Android версии 4.4 и выше. |
| Если приложение должно поддерживать {@link android.content.Intent#ACTION_GET_CONTENT}, |
| чтобы обслуживать устройства, работающие под управлением Android 4.3 и ниже, необходимо |
| отключить фильтр намерения {@link android.content.Intent#ACTION_GET_CONTENT} в |
| манифесте для устройств с Android версии 4.4 и выше. Поставщик |
| документов и намерение {@link android.content.Intent#ACTION_GET_CONTENT} следует считать |
| взаимоисключающими. Если приложение поддерживает их одновременно, оно |
| будет появляться в пользовательском интерфейсе системного элемента выбора дважды, предлагая два различных способа доступа |
| к сохраненным данным. Это запутает пользователей.</p> |
| |
| <p>Отключать фильтр намерения |
| {@link android.content.Intent#ACTION_GET_CONTENT} на устройствах |
| с Android версии 4.4 и выше рекомендуется следующим образом:</p> |
| |
| <ol> |
| <li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values/}, добавить |
| следующую строку: <pre><bool name="atMostJellyBeanMR2">true</bool></pre></li> |
| |
| <li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить |
| следующую строку: <pre><bool name="atMostJellyBeanMR2">false</bool></pre></li> |
| |
| <li>Добавить |
| <a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">псевдоним |
| операции</a>, чтобы отключить фильтр намерения {@link android.content.Intent#ACTION_GET_CONTENT} |
| для версий 4.4 (API уровня 19) и выше. Например: |
| |
| <pre> |
| <!-- This activity alias is added so that GET_CONTENT intent-filter |
| can be disabled for builds on API level 19 and higher. --> |
| <activity-alias android:name="com.android.example.app.MyPicker" |
| android:targetActivity="com.android.example.app.MyActivity" |
| ... |
| android:enabled="@bool/atMostJellyBeanMR2"> |
| <intent-filter> |
| <action android:name="android.intent.action.GET_CONTENT" /> |
| <category android:name="android.intent.category.OPENABLE" /> |
| <category android:name="android.intent.category.DEFAULT" /> |
| <data android:mimeType="image/*" /> |
| <data android:mimeType="video/*" /> |
| </intent-filter> |
| </activity-alias> |
| </pre> |
| </li> |
| </ol> |
| <h3 id="contract">Контракты</h3> |
| |
| <p>Как правило, при создании нестандартного поставщика контента одной из задач |
| является реализация классов-контрактов, описанная в руководстве для разработчиков |
| <a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass"> |
| Поставщики контента</a>. Класс-контракт представляет собой класс {@code public final}, |
| в котором содержатся определения констант для URI, имен столбцов, типов MIME и |
| других метаданных поставщика. Платформа SAF |
| предоставляет разработчику следующие классы-контракты, так что ему не нужно писать |
| собственные:</p> |
| |
| <ul> |
| <li>{@link android.provider.DocumentsContract.Document}</li> |
| <li>{@link android.provider.DocumentsContract.Root}</li> |
| </ul> |
| |
| <p>Например, когда |
| к поставщику документов приходит запрос на документы или корневой каталог, можно возвращать в курсоре следующие столбцы:</p> |
| |
| <pre>private static final String[] DEFAULT_ROOT_PROJECTION = |
| new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES, |
| Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, |
| Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, |
| Root.COLUMN_AVAILABLE_BYTES,}; |
| private static final String[] DEFAULT_DOCUMENT_PROJECTION = new |
| String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, |
| Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, |
| Document.COLUMN_FLAGS, Document.COLUMN_SIZE,}; |
| </pre> |
| |
| <h3 id="subclass">Создание подкласса класса DocumentsProvider</h3> |
| |
| <p>Следующим шагом в разработке собственного поставщика документов является создание подкласса |
| абстрактного класса {@link android.provider.DocumentsProvider}. Как минимум, необходимо |
| реализовать следующие методы:</p> |
| |
| <ul> |
| <li>{@link android.provider.DocumentsProvider#queryRoots queryRoots()}</li> |
| |
| <li>{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}</li> |
| |
| <li>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}</li> |
| |
| <li>{@link android.provider.DocumentsProvider#openDocument openDocument()}</li> |
| </ul> |
| |
| <p>Это единственные методы, реализация которых строго обязательна, однако существует |
| намного больше методов, которые, возможно, тоже придется реализовать. Подробности приводятся в описании класса{@link android.provider.DocumentsProvider} |
| .</p> |
| |
| <h4 id="queryRoots">Реализация метода queryRoots</h4> |
| |
| <p>Реализация метода {@link android.provider.DocumentsProvider#queryRoots |
| queryRoots()} должна возвращать объект {@link android.database.Cursor}, указывающий на все |
| корневые каталоги поставщиков документов, используя столбцы, определенные в |
| {@link android.provider.DocumentsContract.Root}.</p> |
| |
| <p>В следующем фрагменте кода параметр {@code projection} представляет |
| конкретные поля, нужные вызывающему объекту. В этом коде создается курсор, |
| и к нему добавляется одна строка, соответствующая одному корневому каталогу (каталогу верхнего уровня), например, |
| Загрузки или Изображения. Большинство поставщиков имеет только один корневой каталог. Однако ничто не мешает иметь несколько корневых каталогов, |
| например, при наличии нескольких учетных записей. В этом случае достаточно добавить в |
| курсор еще одну строку.</p> |
| |
| <pre> |
| @Override |
| public Cursor queryRoots(String[] projection) throws FileNotFoundException { |
| |
| // Create a cursor with either the requested fields, or the default |
| // projection if "projection" is null. |
| final MatrixCursor result = |
| new MatrixCursor(resolveRootProjection(projection)); |
| |
| // If user is not logged in, return an empty root cursor. This removes our |
| // provider from the list entirely. |
| if (!isUserLoggedIn()) { |
| return result; |
| } |
| |
| // It's possible to have multiple roots (e.g. for multiple accounts in the |
| // same app) -- just add multiple cursor rows. |
| // Construct one row for a root called "MyCloud". |
| final MatrixCursor.RowBuilder row = result.newRow(); |
| row.add(Root.COLUMN_ROOT_ID, ROOT); |
| row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary)); |
| |
| // FLAG_SUPPORTS_CREATE means at least one directory under the root supports |
| // creating documents. FLAG_SUPPORTS_RECENTS means your application's most |
| // recently used documents will show up in the "Recents" category. |
| // FLAG_SUPPORTS_SEARCH allows users to search all documents the application |
| // shares. |
| row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | |
| Root.FLAG_SUPPORTS_RECENTS | |
| Root.FLAG_SUPPORTS_SEARCH); |
| |
| // COLUMN_TITLE is the root title (e.g. Gallery, Drive). |
| row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title)); |
| |
| // This document id cannot change once it's shared. |
| row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir)); |
| |
| // The child MIME types are used to filter the roots and only present to the |
| // user roots that contain the desired type somewhere in their file hierarchy. |
| row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir)); |
| row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace()); |
| row.add(Root.COLUMN_ICON, R.drawable.ic_launcher); |
| |
| return result; |
| }</pre> |
| |
| <h4 id="queryChildDocuments">Реализация метода queryChildDocuments</h4> |
| |
| <p>Реализация метода |
| {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} |
| должна возвращать объект{@link android.database.Cursor}, указывающий на все файлы в |
| заданном каталоге, используя столбцы, определенные в |
| {@link android.provider.DocumentsContract.Document}.</p> |
| |
| <p>Этот метод вызывается, когда в интерфейсе элемента выбора пользователь выбирает корневой каталог приложения. |
| Метод получает документы-потомки каталога на уровне ниже корневого. Его можно вызывать на любом уровне |
| файловой иерархии, а не только в корневом каталоге. В следующем фрагменте кода |
| создается курсор с запрошенными столбцами. Затем в него заносится информация о |
| каждом ближайшем потомке родительского каталога. |
| Потомком может быть изображение, еще один каталог, в общем, любой файл:</p> |
| |
| <pre>@Override |
| public Cursor queryChildDocuments(String parentDocumentId, String[] projection, |
| String sortOrder) throws FileNotFoundException { |
| |
| final MatrixCursor result = new |
| MatrixCursor(resolveDocumentProjection(projection)); |
| final File parent = getFileForDocId(parentDocumentId); |
| for (File file : parent.listFiles()) { |
| // Adds the file's display name, MIME type, size, and so on. |
| includeFile(result, null, file); |
| } |
| return result; |
| } |
| </pre> |
| |
| <h4 id="queryDocument">Реализация метода queryDocument</h4> |
| |
| <p>Реализация метода |
| {@link android.provider.DocumentsProvider#queryDocument queryDocument()} |
| должна возвращать объект{@link android.database.Cursor}, указывающий на заданный файл, |
| используя столбцы, определенные в{@link android.provider.DocumentsContract.Document}. |
| </p> |
| |
| <p>Метод {@link android.provider.DocumentsProvider#queryDocument queryDocument()} |
| возвращает ту же информацию, которую возвращал |
| {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}, |
| но для конкретного файла:</p> |
| |
| |
| <pre>@Override |
| public Cursor queryDocument(String documentId, String[] projection) throws |
| FileNotFoundException { |
| |
| // Create a cursor with the requested projection, or the default projection. |
| final MatrixCursor result = new |
| MatrixCursor(resolveDocumentProjection(projection)); |
| includeFile(result, documentId, null); |
| return result; |
| } |
| </pre> |
| |
| <h4 id="openDocument">Реализация метода openDocument</h4> |
| |
| <p>Необходимо реализовать метод {@link android.provider.DocumentsProvider#openDocument |
| openDocument()}, который возвращает объект {@link android.os.ParcelFileDescriptor}, представляющий |
| указанный файл. Другие приложения смогут воспользоваться возращенным объектом {@link android.os.ParcelFileDescriptor} |
| для организации потока данных. Система вызывает этот метод, когда пользователь выбирает файл, |
| и клиентское приложение запрашивает доступ нему, вызывая |
| метод {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}. |
| Например:</p> |
| |
| <pre>@Override |
| public ParcelFileDescriptor openDocument(final String documentId, |
| final String mode, |
| CancellationSignal signal) throws |
| FileNotFoundException { |
| Log.v(TAG, "openDocument, mode: " + mode); |
| // It's OK to do network operations in this method to download the document, |
| // as long as you periodically check the CancellationSignal. If you have an |
| // extremely large file to transfer from the network, a better solution may |
| // be pipes or sockets (see ParcelFileDescriptor for helper methods). |
| |
| final File file = getFileForDocId(documentId); |
| |
| final boolean isWrite = (mode.indexOf('w') != -1); |
| if(isWrite) { |
| // Attach a close listener if the document is opened in write mode. |
| try { |
| Handler handler = new Handler(getContext().getMainLooper()); |
| return ParcelFileDescriptor.open(file, accessMode, handler, |
| new ParcelFileDescriptor.OnCloseListener() { |
| @Override |
| public void onClose(IOException e) { |
| |
| // Update the file with the cloud server. The client is done |
| // writing. |
| Log.i(TAG, "A file with id " + |
| documentId + " has been closed! |
| Time to " + |
| "update the server."); |
| } |
| |
| }); |
| } catch (IOException e) { |
| throw new FileNotFoundException("Failed to open document with id " |
| + documentId + " and mode " + mode); |
| } |
| } else { |
| return ParcelFileDescriptor.open(file, accessMode); |
| } |
| } |
| </pre> |
| |
| <h3 id="security">Безопасность</h3> |
| |
| <p>Предположим, что поставщик документов представляет собой защищенную паролем службу хранения в облаке, |
| а приложение должно убедиться, что пользователь вошел в систему, прежде чем оно предоставит ему доступ к файлам. |
| Что должно предпринять приложение, если пользователь не выполнил вход? Решение состоит в том, чтобы |
| реализация метода {@link android.provider.DocumentsProvider#queryRoots |
| queryRoots()} не возвращала корневых каталогов. Иными словами, это должен быть пустой корневой курсор:</p> |
| |
| <pre> |
| public Cursor queryRoots(String[] projection) throws FileNotFoundException { |
| ... |
| // If user is not logged in, return an empty root cursor. This removes our |
| // provider from the list entirely. |
| if (!isUserLoggedIn()) { |
| return result; |
| } |
| </pre> |
| |
| <p>Следующий шаг состоит в вызове метода {@code getContentResolver().notifyChange()}. |
| Помните объект {@link android.provider.DocumentsContract}? Воспользуемся им для создания |
| соответствующего URI. В следующем фрагменте кода система извещается о необходимости опрашивать корневые каталоги |
| поставщика документов, когда меняется статус входа пользователя в систему. Если пользователь не |
| выполнил вход, метод {@link android.provider.DocumentsProvider#queryRoots queryRoots()} возвратит |
| пустой курсор, как показано выше. Это гарантирует, что документы поставщика будут |
| доступны только пользователям, вошедшим в поставщик.</p> |
| |
| <pre>private void onLoginButtonClick() { |
| loginOrLogout(); |
| getContentResolver().notifyChange(DocumentsContract |
| .buildRootsUri(AUTHORITY), null); |
| } |
| </pre> |