• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1page.title=Платформа доступа к хранилищу (Storage Access Framework)
2@jd:body
3<div id="qv-wrapper">
4<div id="qv">
5
6<h2>Содержание документа
7<a href="#" onclick="hideNestedItems('#toc44',this);return false;" class="header-toggle">
8        <span class="more">больше информации</span>
9        <span class="less" style="display:none">меньше информации</span></a></h2>
10<ol id="toc44" class="hide-nested">
11    <li>
12        <a href="#overview">Обзор</a>
13    </li>
14    <li>
15        <a href="#flow">Поток управления</a>
16    </li>
17    <li>
18        <a href="#client">Создание клиентского приложения</a>
19        <ol>
20        <li><a href="#search">Поиск документов</a></li>
21        <li><a href="#process">Обработка результатов</a></li>
22        <li><a href="#metadata">Изучение метаданных документа</a></li>
23        <li><a href="#open">Открытие документа</a></li>
24        <li><a href="#create">Создание нового документа</a></li>
25        <li><a href="#delete">Удаление документа</a></li>
26        <li><a href="#edit">Редактирование документа</a></li>
27        <li><a href="#permissions">Удержание прав доступа</a></li>
28        </ol>
29    </li>
30    <li><a href="#custom">Создание собственного поставщика документов</a>
31        <ol>
32        <li><a href="#manifest">Манифест</a></li>
33        <li><a href="#contract">Контракты</a></li>
34        <li><a href="#subclass">Создание подкласса класса DocumentsProvider</a></li>
35        <li><a href="#security">Безопасность</a></li>
36        </ol>
37    </li>
38
39</ol>
40<h2>Ключевые классы</h2>
41<ol>
42    <li>{@link android.provider.DocumentsProvider}</li>
43    <li>{@link android.provider.DocumentsContract}</li>
44</ol>
45
46<h2>Видео</h2>
47
48<ol>
49    <li><a href="http://www.youtube.com/watch?v=zxHVeXbK1P4">
50DevBytes: Android 4.4 Storage Access Framework: Поставщик</a></li>
51     <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ">
52DevBytes: Android 4.4 Storage Access Framework: Клиент</a></li>
53</ol>
54
55
56<h2>Примеры кода</h2>
57
58<ol>
59    <li><a href="{@docRoot}samples/StorageProvider/index.html">
60Класс StorageProvider</a></li>
61     <li><a href="{@docRoot}samples/StorageClient/index.html">
62Класс StorageClient</a></li>
63</ol>
64
65<h2>См. также:</h2>
66<ol>
67    <li>
68        <a href="{@docRoot}guide/topics/providers/content-provider-basics.html">
69Основные сведения о поставщике контента
70</a>
71    </li>
72</ol>
73
74</div>
75</div>
76
77
78<p>Платформа доступа к хранилищу (Storage Access Framework, SAF) впервые появилась в Android версии 4.4 (API уровня 19). Платформа SAF
79 облегчает пользователям поиск и открытие документов, изображений и других файлов
80в хранилищах всех поставщиков, с которыми они работают. Стандартный удобный интерфейс
81позволяет пользователям применять единый для всех приложений и поставщиков способ поиска файлов и доступа к последним добавленным файлам.</p>
82
83<p>Облачные или локальные службы хранения могут присоединиться к этой экосистеме, реализовав
84класс {@link android.provider.DocumentsProvider}, инкапсулирующий их услуги. Клиентские
85приложения, которым требуется доступ к документам поставщика, могут интегрироваться с SAF с помощью всего нескольких
86строчек кода.</p>
87
88<p>Платформа SAF включает в себя следующие компоненты:</p>
89
90<ul>
91<li><strong>Поставщик документов</strong>&mdash;поставщик контента, позволяющий
92службе хранения (например, Диск Google) показывать файлы, которыми он управляет. Поставщик документов
93реализуется как подкласс класса{@link android.provider.DocumentsProvider}.
94Его схема основана на традиционной файловой иерархии,
95однако физический способ хранения данных в поставщике документов остается на усмотрении разработчика.
96 Платформа Android включает в себя несколько встроенных поставщиков документов, таких как
97Загрузки, Изображения и Видео.</li>
98
99<li><strong>Клиентское приложение</strong>&mdash;пользовательское приложение, вызывающее намерение
100{@link android.content.Intent#ACTION_OPEN_DOCUMENT} и/или
101{@link android.content.Intent#ACTION_CREATE_DOCUMENT} и принимающее
102файлы, возвращаемые поставщиками документов.</li>
103
104<li><strong>Элемент выбора</strong>&mdash;системный пользовательский интерфейс, обеспечивающий пользователям доступ к документам у всех
105поставщиков документов, которые удовлетворяют критериям поиска, заданным в клиентском приложении.</li>
106</ul>
107
108<p>Платформа SAF в числе прочих предоставляет следующие функции:</p>
109<ul>
110<li>позволяет пользователям искать контент у всех поставщиков документов, а не только у одного приложения;</li>
111<li>обеспечивает приложению возможность долговременного, постоянного доступа к
112 документам, принадлежащим поставщику документов. Благодаря такому доступу пользователи могут добавлять, редактировать,
113 сохранять и удалять файлы, хранящиеся у поставщика;</li>
114<li>поддерживает несколько учетных записей и временные корневые каталоги, например, поставщики
115на USB-накопителях, которые появляются, только когда накопитель вставлен в порт. </li>
116</ul>
117
118<h2 id ="overview">Обзор</h2>
119
120<p>В центре платформы SAF находится поставщик контента, являющийся
121подклассом класса {@link android.provider.DocumentsProvider}. Внутри <em>поставщика документов</em>данные имеют
122структуру традиционной файловой иерархии:</p>
123<p><img src="{@docRoot}images/providers/storage_datamodel.png" alt="data model" /></p>
124<p class="img-caption"><strong>Рисунок 1.</strong> Модель данных поставщика документов. На рисунке Root (Корневой каталог) указывает на один объект Document (Документ),
125который затем разветвляется в целое дерево.</p>
126
127<p>Обратите внимание на следующее.</p>
128<ul>
129
130<li>Каждый поставщик документов предоставляет один или несколько
131«корневых каталогов», являющихся отправными точками при обходе дерева документов.
132Каждый корневой каталог имеет уникальный идентификатор {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID}
133и указывает на документ (каталог),
134представляющий содержимое на уровне ниже корневого.
135Корневые каталоги динамичны по своей конструкции, чтобы обеспечивать поддержку таким вариантам использования, как несколько учетных записей,
136временные хранилища на USB-нкопителях и возможность для пользователя войти в систему и выйти из нее.</li>
137
138<li>В каждом корневом каталоге находится один документ. Этот документ указывает на количество документов <em>N</em>
139каждый из которых, в свою очередь, может указывать на один или <em>N</em> документов. </li>
140
141<li>Каждый сервер хранилища показывает
142отдельные файлы и каталоги, ссылаясь на них с помощью уникального
143идентификатора {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID}.
144Идентификаторы документов должны быть уникальными и не меняться после присвоения, поскольку они используются для выдачи постоянных
145URI, не зависящих от перезагрузки устройства.</li>
146
147
148<li>Документ — это или открываемый файл (имеющий конкретный MIME-тип), или
149каталог, содержащий другие документы (с
150MIME-типом {@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR}).</li>
151
152<li>Каждый документ может иметь различные свойства, описываемые флагами
153{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS},
154такими как{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE},
155{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE} и
156{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL}.
157Документ с одним и тем же идентификатором {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} может находиться
158в нескольких каталогах.</li>
159</ul>
160
161<h2 id="flow">Поток управления</h2>
162<p>Как было сказано выше, модель данных поставщика документов основана на традиционной
163файловой иерархии. Однако физический способ хранения данных остается на усмотрение разработчика, при
164условии, что к ним можно обращаться через API-интерфейс {@link android.provider.DocumentsProvider}. Например, можно
165использовать для данных облачное хранилище на основе тегов.</p>
166
167<p>На рисунке 2 показан пример того, как приложение для обработки фотографий может использовать SAF
168для доступа к сохраненным данным:</p>
169<p><img src="{@docRoot}images/providers/storage_dataflow.png" alt="app" /></p>
170
171<p class="img-caption"><strong>Рисунок 2.</strong> Поток управления Storage Access Framework</p>
172
173<p>Обратите внимание на следующее.</p>
174<ul>
175
176<li>На платформе SAF поставщики и клиенты не взаимодействуют
177напрямую. Клиент запрашивает разрешение на взаимодействие
178с файлами (то есть, на чтение, редактирование, создание или удаление файлов).</li>
179
180<li>Взаимодействие начинается, когда приложение (в нашем примере обрабатывающее фотографии) активизирует намерение
181{@link android.content.Intent#ACTION_OPEN_DOCUMENT} или {@link android.content.Intent#ACTION_CREATE_DOCUMENT}. Намерение может включать в себя фильтры
182для уточнения критериев, например, «предоставить открываемые файлы
183с MIME-типом image».</li>
184
185<li>Когда намерение срабатывает, системный элемент выбора переходит к каждому зарегистрированному поставщику
186и показывает пользователю корневые каталоги с контентом, соответствующим запросу.</li>
187
188<li>Элемент выбора предоставляет пользователю стандартный интерфейс, даже
189если поставщики документов значительно различаются. В качестве примера на рисунке 2
190изображены Диск Google, поставщик на USB-накопителе и облачный поставщик.</li>
191</ul>
192
193<p>На рисунке 3 показан элемент выбора, в котором пользователь для поиска изображений выбрал учетную запись
194Диск Google:</p>
195
196<p><img src="{@docRoot}images/providers/storage_picker.png" width="340" alt="picker" style="border:2px solid #ddd" /></p>
197
198<p class="img-caption"><strong>Рисунок 3.</strong> Элемент выбора</p>
199
200<p>Когда пользователь выбирает Диск Google, изображения отображаются, как показано на
201рисунке 4. С этого момента пользователь может взаимодействовать с ними любыми способами,
202 которые поддерживаются поставщиком и клиентским приложением.
203
204<p><img src="{@docRoot}images/providers/storage_photos.png" width="340" alt="picker" style="border:2px solid #ddd" /></p>
205
206<p class="img-caption"><strong>Рисунок 4.</strong> Изображения</p>
207
208<h2 id="client">Создание клиентского приложения</h2>
209
210<p>В Android версии 4.3 и ниже для того, чтобы приложение могло получать файл от другого
211приложения, оно должно активизировать намерение, например, {@link android.content.Intent#ACTION_PICK}
212или {@link android.content.Intent#ACTION_GET_CONTENT}. После этого пользователь должен выбрать
213какое-либо одно приложение, чтобы получить файл, а оно должно предоставить пользователю
214интерфейс, с помощью которого он сможет выбирать и получать файлы. </p>
215
216<p>Начиная с Android 4.4 и выше, у разработчика имеется дополнительная возможность — намерение
217{@link android.content.Intent#ACTION_OPEN_DOCUMENT},
218которое отображает пользовательский интерфейс элемента выбора, управляемого системой. Этот элемент предоставляет пользователю
219обзор всех файлов, доступных в других приложениях. Благодаря этому единому интерфейсу,
220пользователь может выбрать файл в любом из поддерживаемых приложений.</p>
221
222<p>Намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT} не
223является заменой для намерения {@link android.content.Intent#ACTION_GET_CONTENT}.
224 Разработчику следует использовать то, которое лучше соответствует потребностям приложения:</p>
225
226<ul>
227<li>используйте {@link android.content.Intent#ACTION_GET_CONTENT}, если приложению нужно просто
228прочитать или импортировать данные. При таком подходе приложение импортирует копию данных,
229 например, файл с изображением.</li>
230
231<li>используйте {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, если
232приложению нужна возможность долговременного, постоянного доступа к документам, принадлежащим поставщику
233документов. В качестве примера можно назвать редактор фотографий, позволяющий пользователям обрабатывать
234изображения, хранящиеся в поставщике документов. </li>
235
236</ul>
237
238
239<p>В этом разделе показано, как написать клиентское приложение, использующее намерения
240{@link android.content.Intent#ACTION_OPEN_DOCUMENT} и
241{@link android.content.Intent#ACTION_CREATE_DOCUMENT}.</p>
242
243
244<h3 id="search">Поиск документов</h3>
245
246<p>
247В следующем фрагменте кода намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
248используется для поиска поставщиков документов,
249содержащих файлы изображений:</p>
250
251<pre>private static final int READ_REQUEST_CODE = 42;
252...
253/**
254 * Fires an intent to spin up the &quot;file chooser&quot; UI and select an image.
255 */
256public void performFileSearch() {
257
258    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
259    // browser.
260    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
261
262    // Filter to only show results that can be &quot;opened&quot;, such as a
263    // file (as opposed to a list of contacts or timezones)
264    intent.addCategory(Intent.CATEGORY_OPENABLE);
265
266    // Filter to show only images, using the image MIME data type.
267    // If one wanted to search for ogg vorbis files, the type would be &quot;audio/ogg&quot;.
268    // To search for all documents available via installed storage providers,
269    // it would be &quot;*/*&quot;.
270    intent.setType(&quot;image/*&quot;);
271
272    startActivityForResult(intent, READ_REQUEST_CODE);
273}</pre>
274
275<p>Обратите внимание на следующее.</p>
276<ul>
277<li>Когда приложение активизирует намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
278, оно запускает элемент выбора, отображающий всех поставщиков документов, соответствующих заданным критериям.</li>
279
280<li>Добавление категории {@link android.content.Intent#CATEGORY_OPENABLE} в
281фильтры намерения приводит к отображению только тех документов, которые можно открыть, например, файлов с изображениями.</li>
282
283<li>Оператор {@code intent.setType("image/*")} выполняет дальнейшую фильтрацию, чтобы
284отображались только документы с MIME-типом image.</li>
285</ul>
286
287<h3 id="results">Обработка результатов</h3>
288
289<p>Когда пользователь выбирает документ в элементе выбора,
290вызывается метод {@link android.app.Activity#onActivityResult onActivityResult()}.
291Идентификатор URI, указывающий на выбранный документ, содержится в параметре{@code resultData}.
292 Чтобы извлечь URI, следует вызвать {@link android.content.Intent#getData getData()}.
293Этот URI можно использовать для получения документа, нужного пользователю. Например:
294</p>
295
296<pre>&#64;Override
297public void onActivityResult(int requestCode, int resultCode,
298        Intent resultData) {
299
300    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
301    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
302    // response to some other intent, and the code below shouldn't run at all.
303
304    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
305        // The document selected by the user won't be returned in the intent.
306        // Instead, a URI to that document will be contained in the return intent
307        // provided to this method as a parameter.
308        // Pull that URI using resultData.getData().
309        Uri uri = null;
310        if (resultData != null) {
311            uri = resultData.getData();
312            Log.i(TAG, "Uri: " + uri.toString());
313            showImage(uri);
314        }
315    }
316}
317</pre>
318
319<h3 id="metadata">Изучение метаданных документа</h3>
320
321<p>Имея в своем распоряжении URI документа, разработчик получает доступ к его метаданным. В следующем
322фрагменте кода метаданные документа, определяемого идентификатором URI, считываются и записываются в журнал:</p>
323
324<pre>public void dumpImageMetaData(Uri uri) {
325
326    // The query, since it only applies to a single document, will only return
327    // one row. There's no need to filter, sort, or select fields, since we want
328    // all fields for one document.
329    Cursor cursor = getActivity().getContentResolver()
330            .query(uri, null, null, null, null, null);
331
332    try {
333    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
334    // &quot;if there's anything to look at, look at it&quot; conditionals.
335        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
336
337            // Note it's called &quot;Display Name&quot;.  This is
338            // provider-specific, and might not necessarily be the file name.
339            String displayName = cursor.getString(
340                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
341            Log.i(TAG, &quot;Display Name: &quot; + displayName);
342
343            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
344            // If the size is unknown, the value stored is null.  But since an
345            // int can't be null in Java, the behavior is implementation-specific,
346            // which is just a fancy term for &quot;unpredictable&quot;.  So as
347            // a rule, check if it's null before assigning to an int.  This will
348            // happen often:  The storage API allows for remote files, whose
349            // size might not be locally known.
350            String size = null;
351            if (!cursor.isNull(sizeIndex)) {
352                // Technically the column stores an int, but cursor.getString()
353                // will do the conversion automatically.
354                size = cursor.getString(sizeIndex);
355            } else {
356                size = &quot;Unknown&quot;;
357            }
358            Log.i(TAG, &quot;Size: &quot; + size);
359        }
360    } finally {
361        cursor.close();
362    }
363}
364</pre>
365
366<h3 id="open-client">Открытие документа</h3>
367
368<p>Получив URI документа, разработчик может открывать его и в целом
369делать с ним всё, что угодно.</p>
370
371<h4>Объект растровых изображений</h4>
372
373<p>Приведем пример кода для открытия объекта {@link android.graphics.Bitmap}:</p>
374
375<pre>private Bitmap getBitmapFromUri(Uri uri) throws IOException {
376    ParcelFileDescriptor parcelFileDescriptor =
377            getContentResolver().openFileDescriptor(uri, "r");
378    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
379    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
380    parcelFileDescriptor.close();
381    return image;
382}
383</pre>
384
385<p>Обратите внимание, что не следует производить эту операцию в потоке пользовательского интерфейса. Ее нужно выполнять
386в фоне, с помощью {@link android.os.AsyncTask}. Когда файл с растровым изображением откроется, его
387можно отобразить в виджете {@link android.widget.ImageView}.
388</p>
389
390<h4>Получение объекта InputStream</h4>
391
392<p>Далее приведен пример того, как можно получить объект {@link java.io.InputStream} по идентификатору URI. В этом
393фрагменте кода строчки файла считываются в объект строкового типа:</p>
394
395<pre>private String readTextFromUri(Uri uri) throws IOException {
396    InputStream inputStream = getContentResolver().openInputStream(uri);
397    BufferedReader reader = new BufferedReader(new InputStreamReader(
398            inputStream));
399    StringBuilder stringBuilder = new StringBuilder();
400    String line;
401    while ((line = reader.readLine()) != null) {
402        stringBuilder.append(line);
403    }
404    fileInputStream.close();
405    parcelFileDescriptor.close();
406    return stringBuilder.toString();
407}
408</pre>
409
410<h3 id="create">Создание нового документа</h3>
411
412<p>Приложение может создать новый документ в поставщике документов, используя намерение
413{@link android.content.Intent#ACTION_CREATE_DOCUMENT}
414. Чтобы создать файл, нужно указать в намерении MIME-тип и имя файла, а затем
415запустить его с уникальным кодом запроса. Об остальном позаботится платформа:</p>
416
417
418<pre>
419// Here are some examples of how you might call this method.
420// The first parameter is the MIME type, and the second parameter is the name
421// of the file you are creating:
422//
423// createFile("text/plain", "foobar.txt");
424// createFile("image/png", "mypicture.png");
425
426// Unique request code.
427private static final int WRITE_REQUEST_CODE = 43;
428...
429private void createFile(String mimeType, String fileName) {
430    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
431
432    // Filter to only show results that can be &quot;opened&quot;, such as
433    // a file (as opposed to a list of contacts or timezones).
434    intent.addCategory(Intent.CATEGORY_OPENABLE);
435
436    // Create a file with the requested MIME type.
437    intent.setType(mimeType);
438    intent.putExtra(Intent.EXTRA_TITLE, fileName);
439    startActivityForResult(intent, WRITE_REQUEST_CODE);
440}
441</pre>
442
443<p>После создания нового документа можно получить его URI с помощью
444метода {@link android.app.Activity#onActivityResult onActivityResult()}, чтобы иметь возможность
445записывать в него данные.</p>
446
447<h3 id="delete">Удаление документа</h3>
448
449<p>Если у разработчика имеется URI документа, а объект
450{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS}
451этого документа содержит флаг
452{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE},
453то документ можно удалить. Например:</p>
454
455<pre>
456DocumentsContract.deleteDocument(getContentResolver(), uri);
457</pre>
458
459<h3 id="edit">Редактирование документа</h3>
460
461<p>Платформа SAF позволяет редактировать текстовые документы на месте.
462В следующем фрагменте кода активизируется
463намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, а
464категория {@link android.content.Intent#CATEGORY_OPENABLE} используется, чтобы отображались только
465документы, которые можно открыть. Затем производится дальнейшая фильтрация, чтобы отображались только текстовые файлы:</p>
466
467<pre>
468private static final int EDIT_REQUEST_CODE = 44;
469/**
470 * Open a file for writing and append some text to it.
471 */
472 private void editDocument() {
473    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
474    // file browser.
475    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
476
477    // Filter to only show results that can be &quot;opened&quot;, such as a
478    // file (as opposed to a list of contacts or timezones).
479    intent.addCategory(Intent.CATEGORY_OPENABLE);
480
481    // Filter to show only text files.
482    intent.setType(&quot;text/plain&quot;);
483
484    startActivityForResult(intent, EDIT_REQUEST_CODE);
485}
486</pre>
487
488<p>Далее, из метода {@link android.app.Activity#onActivityResult onActivityResult()}
489(см. <a href="#results">Обработка результатов</a>) можно вызвать код для выполнения редактирования.
490В следующем фрагменте кода объект {@link java.io.FileOutputStream}
491получен с помощью объекта класса {@link android.content.ContentResolver}. По умолчанию используется режим записи.
492Рекомендуется запрашивать минимально необходимые права доступа, поэтому не следует запрашивать
493чтение/запись, если приложению требуется только записать файл:</p>
494
495<pre>private void alterDocument(Uri uri) {
496    try {
497        ParcelFileDescriptor pfd = getActivity().getContentResolver().
498                openFileDescriptor(uri, "w");
499        FileOutputStream fileOutputStream =
500                new FileOutputStream(pfd.getFileDescriptor());
501        fileOutputStream.write(("Overwritten by MyCloud at " +
502                System.currentTimeMillis() + "\n").getBytes());
503        // Let the document provider know you're done by closing the stream.
504        fileOutputStream.close();
505        pfd.close();
506    } catch (FileNotFoundException e) {
507        e.printStackTrace();
508    } catch (IOException e) {
509        e.printStackTrace();
510    }
511}</pre>
512
513<h3 id="permissions">Удержание прав доступа</h3>
514
515<p>Когда приложение открывает файл для чтения или записи, система предоставляет
516ему URI-разрешение на этот файл. Разрешение действует вплоть до перезагрузки устройства.
517Предположим, что в графическом редакторе требуется, чтобы у пользователя была возможность
518открыть непосредственно в этом приложении последние пять изображений, которые он редактировал. Если он
519перезапустил устройство, возникает необходимость снова отсылать его к системному элементу выбора для поиска
520файлов. Очевидно, это далеко не идеальный вариант.</p>
521
522<p>Чтобы избежать такой ситуации, разработчик может удержать права доступа, предоставленные системой
523его приложению. Приложение фактически принимает постоянное URI-разрешение,
524предлагаемое системой. В результате пользователь получает непрерывный доступ к файлам
525из приложения, независимо от перезагрузки устройства:</p>
526
527
528<pre>final int takeFlags = intent.getFlags()
529            &amp; (Intent.FLAG_GRANT_READ_URI_PERMISSION
530            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
531// Check for the freshest data.
532getContentResolver().takePersistableUriPermission(uri, takeFlags);</pre>
533
534<p>Остается один заключительный шаг. Можно сохранить последние
535URI-идентификаторы, с которыми работало приложение. Однако не исключено, что они потеряют актуальность, поскольку другое приложение
536может удалить или модифицировать документ. Поэтому следует всегда вызывать
537{@code getContentResolver().takePersistableUriPermission()}, чтобы получать
538актуальные данные.</p>
539
540<h2 id="custom">Создание собственного поставщика документов</h2>
541
542<p>
543При разработке приложения, оказывающего услуги по хранению файлов (например,
544службы хранения в облаке), можно предоставить доступ к файлам при помощи
545SAF, написав собственный поставщик документов.  В этом разделе показано,
546как это сделать.</p>
547
548
549<h3 id="manifest">Манифест</h3>
550
551<p>Чтобы реализовать собственный поставщик документов, необходимо добавить в манифест приложения
552следующую информацию:</p>
553<ul>
554
555<li>Целевой API-интерфейс уровня 19 или выше.</li>
556
557<li>Элемент <code>&lt;provider&gt;</code>, в котором объявляется нестандартный поставщик
558 хранилища. </li>
559
560<li>Имя поставщика, т. е., имя его класса с именем пакета.
561Например: <code>com.example.android.storageprovider.MyCloudProvider</code>.</li>
562
563<li>Имя центра поставщика, т. е. имя пакета (в этом примере —
564<code>com.example.android.storageprovider</code>) с типом поставщика контента
565(<code>documents</code>). Например,{@code com.example.android.storageprovider.documents}.</li>
566
567<li>Атрибут <code>android:exported</code>, установленный в значение <code>&quot;true&quot;</code>.
568 Необходимо экспортировать поставщик, чтобы он был виден другим приложениям.</li>
569
570<li>Атрибут <code>android:grantUriPermissions</code>, установленный в значение
571<code>&quot;true&quot;</code>. Этот параметр позволяет системе предоставлять другим приложениям доступ
572к контенту поставщика. Обсуждение того, как следует удерживать права доступа
573к конкретному документу см. в разделе <a href="#permissions">Удержание прав доступа</a>.</li>
574
575<li>Разрешение {@code MANAGE_DOCUMENTS}. По умолчанию поставщик доступен
576всем. Добавление этого разрешения в манифест делает поставщик доступным только системе.
577Это важно для обеспечения безопасности.</li>
578
579<li>Атрибут {@code android:enabled}, имеющий логическое значение, определенное в файле
580ресурсов. Этот атрибут предназначен для отключения поставщика на устройствах под управлением Android версии 4.3 и ниже.
581 Например: {@code android:enabled="@bool/atLeastKitKat"}. Помимо
582включения этого атрибута в манифест, необходимо сделать следующее:
583<ul>
584<li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values/}, добавить
585строчку <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;false&lt;/bool&gt;</pre></li>
586
587<li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить
588строчку <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;true&lt;/bool&gt;</pre></li>
589</ul></li>
590
591<li>Фильтр намерения с действием
592{@code android.content.action.DOCUMENTS_PROVIDER}, чтобы поставщик
593появлялся в элементе выбора, когда система будет искать поставщиков.</li>
594
595</ul>
596<p>Ниже приведены отрывки из образца манифеста, включающего в себя поставщик:</p>
597
598<pre>&lt;manifest... &gt;
599    ...
600    &lt;uses-sdk
601        android:minSdkVersion=&quot;19&quot;
602        android:targetSdkVersion=&quot;19&quot; /&gt;
603        ....
604        &lt;provider
605            android:name=&quot;com.example.android.storageprovider.MyCloudProvider&quot;
606            android:authorities=&quot;com.example.android.storageprovider.documents&quot;
607            android:grantUriPermissions=&quot;true&quot;
608            android:exported=&quot;true&quot;
609            android:permission=&quot;android.permission.MANAGE_DOCUMENTS&quot;
610            android:enabled=&quot;&#64;bool/atLeastKitKat&quot;&gt;
611            &lt;intent-filter&gt;
612                &lt;action android:name=&quot;android.content.action.DOCUMENTS_PROVIDER&quot; /&gt;
613            &lt;/intent-filter&gt;
614        &lt;/provider&gt;
615    &lt;/application&gt;
616
617&lt;/manifest&gt;</pre>
618
619<h4 id="43">Поддержка устройств под управлением Android версии 4.3 и ниже</h4>
620
621<p>Намерение
622{@link android.content.Intent#ACTION_OPEN_DOCUMENT} доступно только
623на устройствах с Android версии 4.4 и выше.
624Если приложение должно поддерживать {@link android.content.Intent#ACTION_GET_CONTENT},
625чтобы обслуживать устройства, работающие под управлением Android 4.3 и ниже, необходимо
626отключить фильтр намерения {@link android.content.Intent#ACTION_GET_CONTENT} в
627манифесте для устройств с Android версии 4.4 и выше. Поставщик
628документов и намерение {@link android.content.Intent#ACTION_GET_CONTENT} следует считать
629взаимоисключающими. Если приложение поддерживает их одновременно, оно
630будет появляться в пользовательском интерфейсе системного элемента выбора дважды, предлагая два различных способа доступа
631к сохраненным данным. Это запутает пользователей.</p>
632
633<p>Отключать фильтр намерения
634{@link android.content.Intent#ACTION_GET_CONTENT} на устройствах
635с Android версии 4.4 и выше рекомендуется следующим образом:</p>
636
637<ol>
638<li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values/}, добавить
639следующую строку: <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;true&lt;/bool&gt;</pre></li>
640
641<li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить
642следующую строку: <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;false&lt;/bool&gt;</pre></li>
643
644<li>Добавить
645<a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">псевдоним
646операции</a>, чтобы отключить фильтр намерения {@link android.content.Intent#ACTION_GET_CONTENT}
647для версий 4.4 (API уровня 19) и выше. Например:
648
649<pre>
650&lt;!-- This activity alias is added so that GET_CONTENT intent-filter
651     can be disabled for builds on API level 19 and higher. --&gt;
652&lt;activity-alias android:name=&quot;com.android.example.app.MyPicker&quot;
653        android:targetActivity=&quot;com.android.example.app.MyActivity&quot;
654        ...
655        android:enabled=&quot;@bool/atMostJellyBeanMR2&quot;&gt;
656    &lt;intent-filter&gt;
657        &lt;action android:name=&quot;android.intent.action.GET_CONTENT&quot; /&gt;
658        &lt;category android:name=&quot;android.intent.category.OPENABLE&quot; /&gt;
659        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
660        &lt;data android:mimeType=&quot;image/*&quot; /&gt;
661        &lt;data android:mimeType=&quot;video/*&quot; /&gt;
662    &lt;/intent-filter&gt;
663&lt;/activity-alias&gt;
664</pre>
665</li>
666</ol>
667<h3 id="contract">Контракты</h3>
668
669<p>Как правило, при создании нестандартного поставщика контента одной из задач
670является реализация классов-контрактов, описанная в руководстве для разработчиков
671<a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass">
672Поставщики контента</a>. Класс-контракт представляет собой класс {@code public final},
673в котором содержатся определения констант для URI, имен столбцов, типов MIME и
674других метаданных поставщика. Платформа SAF
675предоставляет разработчику следующие классы-контракты, так что ему не нужно писать
676собственные:</p>
677
678<ul>
679   <li>{@link android.provider.DocumentsContract.Document}</li>
680   <li>{@link android.provider.DocumentsContract.Root}</li>
681</ul>
682
683<p>Например, когда
684к поставщику документов приходит запрос на документы или корневой каталог, можно возвращать в курсоре следующие столбцы:</p>
685
686<pre>private static final String[] DEFAULT_ROOT_PROJECTION =
687        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
688        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
689        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
690        Root.COLUMN_AVAILABLE_BYTES,};
691private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
692        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
693        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
694        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
695</pre>
696
697<h3 id="subclass">Создание подкласса класса DocumentsProvider</h3>
698
699<p>Следующим шагом в разработке собственного поставщика документов является создание подкласса
700абстрактного класса {@link android.provider.DocumentsProvider}. Как минимум, необходимо
701реализовать следующие методы:</p>
702
703<ul>
704<li>{@link android.provider.DocumentsProvider#queryRoots queryRoots()}</li>
705
706<li>{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}</li>
707
708<li>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}</li>
709
710<li>{@link android.provider.DocumentsProvider#openDocument openDocument()}</li>
711</ul>
712
713<p>Это единственные методы, реализация которых строго обязательна, однако существует
714намного больше методов, которые, возможно, тоже придется реализовать. Подробности приводятся в описании класса{@link android.provider.DocumentsProvider}
715.</p>
716
717<h4 id="queryRoots">Реализация метода queryRoots</h4>
718
719<p>Реализация метода {@link android.provider.DocumentsProvider#queryRoots
720queryRoots()} должна возвращать объект {@link android.database.Cursor}, указывающий на все
721корневые каталоги поставщиков документов, используя столбцы, определенные в
722{@link android.provider.DocumentsContract.Root}.</p>
723
724<p>В следующем фрагменте кода параметр {@code projection} представляет
725конкретные поля, нужные вызывающему объекту. В этом коде создается курсор,
726и к нему добавляется одна строка, соответствующая одному корневому каталогу (каталогу верхнего уровня), например,
727Загрузки или Изображения.  Большинство поставщиков имеет только один корневой каталог. Однако ничто не мешает иметь несколько корневых каталогов,
728например, при наличии нескольких учетных записей. В этом случае достаточно добавить в
729курсор еще одну строку.</p>
730
731<pre>
732&#64;Override
733public Cursor queryRoots(String[] projection) throws FileNotFoundException {
734
735    // Create a cursor with either the requested fields, or the default
736    // projection if "projection" is null.
737    final MatrixCursor result =
738            new MatrixCursor(resolveRootProjection(projection));
739
740    // If user is not logged in, return an empty root cursor.  This removes our
741    // provider from the list entirely.
742    if (!isUserLoggedIn()) {
743        return result;
744    }
745
746    // It's possible to have multiple roots (e.g. for multiple accounts in the
747    // same app) -- just add multiple cursor rows.
748    // Construct one row for a root called &quot;MyCloud&quot;.
749    final MatrixCursor.RowBuilder row = result.newRow();
750    row.add(Root.COLUMN_ROOT_ID, ROOT);
751    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));
752
753    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
754    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
755    // recently used documents will show up in the &quot;Recents&quot; category.
756    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
757    // shares.
758    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
759            Root.FLAG_SUPPORTS_RECENTS |
760            Root.FLAG_SUPPORTS_SEARCH);
761
762    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
763    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));
764
765    // This document id cannot change once it's shared.
766    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));
767
768    // The child MIME types are used to filter the roots and only present to the
769    //  user roots that contain the desired type somewhere in their file hierarchy.
770    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
771    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
772    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
773
774    return result;
775}</pre>
776
777<h4 id="queryChildDocuments">Реализация метода queryChildDocuments</h4>
778
779<p>Реализация метода
780{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}
781должна возвращать объект{@link android.database.Cursor}, указывающий на все файлы в
782заданном каталоге, используя столбцы, определенные в
783{@link android.provider.DocumentsContract.Document}.</p>
784
785<p>Этот метод вызывается, когда в интерфейсе элемента выбора пользователь выбирает корневой каталог приложения.
786Метод получает документы-потомки каталога на уровне ниже корневого.  Его можно вызывать на любом уровне
787файловой иерархии, а не только в корневом каталоге. В следующем фрагменте кода
788создается курсор с запрошенными столбцами. Затем в него заносится информация о
789каждом ближайшем потомке родительского каталога.
790Потомком может быть изображение, еще один каталог, в общем, любой файл:</p>
791
792<pre>&#64;Override
793public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
794                              String sortOrder) throws FileNotFoundException {
795
796    final MatrixCursor result = new
797            MatrixCursor(resolveDocumentProjection(projection));
798    final File parent = getFileForDocId(parentDocumentId);
799    for (File file : parent.listFiles()) {
800        // Adds the file's display name, MIME type, size, and so on.
801        includeFile(result, null, file);
802    }
803    return result;
804}
805</pre>
806
807<h4 id="queryDocument">Реализация метода queryDocument</h4>
808
809<p>Реализация метода
810{@link android.provider.DocumentsProvider#queryDocument queryDocument()}
811должна возвращать объект{@link android.database.Cursor}, указывающий на заданный файл,
812используя столбцы, определенные в{@link android.provider.DocumentsContract.Document}.
813</p>
814
815<p>Метод {@link android.provider.DocumentsProvider#queryDocument queryDocument()}
816возвращает ту же информацию, которую возвращал
817{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()},
818но для конкретного файла:</p>
819
820
821<pre>&#64;Override
822public Cursor queryDocument(String documentId, String[] projection) throws
823        FileNotFoundException {
824
825    // Create a cursor with the requested projection, or the default projection.
826    final MatrixCursor result = new
827            MatrixCursor(resolveDocumentProjection(projection));
828    includeFile(result, documentId, null);
829    return result;
830}
831</pre>
832
833<h4 id="openDocument">Реализация метода openDocument</h4>
834
835<p>Необходимо реализовать метод {@link android.provider.DocumentsProvider#openDocument
836openDocument()}, который возвращает объект {@link android.os.ParcelFileDescriptor}, представляющий
837указанный файл. Другие приложения смогут воспользоваться возращенным объектом {@link android.os.ParcelFileDescriptor}
838для организации потока данных. Система вызывает этот метод, когда пользователь выбирает файл,
839и клиентское приложение запрашивает доступ нему, вызывая
840метод {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}.
841Например:</p>
842
843<pre>&#64;Override
844public ParcelFileDescriptor openDocument(final String documentId,
845                                         final String mode,
846                                         CancellationSignal signal) throws
847        FileNotFoundException {
848    Log.v(TAG, &quot;openDocument, mode: &quot; + mode);
849    // It's OK to do network operations in this method to download the document,
850    // as long as you periodically check the CancellationSignal. If you have an
851    // extremely large file to transfer from the network, a better solution may
852    // be pipes or sockets (see ParcelFileDescriptor for helper methods).
853
854    final File file = getFileForDocId(documentId);
855
856    final boolean isWrite = (mode.indexOf('w') != -1);
857    if(isWrite) {
858        // Attach a close listener if the document is opened in write mode.
859        try {
860            Handler handler = new Handler(getContext().getMainLooper());
861            return ParcelFileDescriptor.open(file, accessMode, handler,
862                        new ParcelFileDescriptor.OnCloseListener() {
863                &#64;Override
864                public void onClose(IOException e) {
865
866                    // Update the file with the cloud server. The client is done
867                    // writing.
868                    Log.i(TAG, &quot;A file with id &quot; +
869                    documentId + &quot; has been closed!
870                    Time to &quot; +
871                    &quot;update the server.&quot;);
872                }
873
874            });
875        } catch (IOException e) {
876            throw new FileNotFoundException(&quot;Failed to open document with id &quot;
877            + documentId + &quot; and mode &quot; + mode);
878        }
879    } else {
880        return ParcelFileDescriptor.open(file, accessMode);
881    }
882}
883</pre>
884
885<h3 id="security">Безопасность</h3>
886
887<p>Предположим, что поставщик документов представляет собой защищенную паролем службу хранения в облаке,
888а приложение должно убедиться, что пользователь вошел в систему, прежде чем оно предоставит ему доступ к файлам.
889Что должно предпринять приложение, если пользователь не выполнил вход?  Решение состоит в том, чтобы
890реализация метода {@link android.provider.DocumentsProvider#queryRoots
891queryRoots()} не возвращала корневых каталогов. Иными словами, это должен быть пустой корневой курсор:</p>
892
893<pre>
894public Cursor queryRoots(String[] projection) throws FileNotFoundException {
895...
896    // If user is not logged in, return an empty root cursor.  This removes our
897    // provider from the list entirely.
898    if (!isUserLoggedIn()) {
899        return result;
900}
901</pre>
902
903<p>Следующий шаг состоит в вызове метода {@code getContentResolver().notifyChange()}.
904Помните объект {@link android.provider.DocumentsContract}?  Воспользуемся им для создания
905соответствующего URI. В следующем фрагменте кода система извещается о необходимости опрашивать корневые каталоги
906поставщика документов, когда меняется статус входа пользователя в систему. Если пользователь не
907выполнил вход, метод {@link android.provider.DocumentsProvider#queryRoots queryRoots()} возвратит
908пустой курсор, как показано выше. Это гарантирует, что документы поставщика будут
909доступны только пользователям, вошедшим в поставщик.</p>
910
911<pre>private void onLoginButtonClick() {
912    loginOrLogout();
913    getContentResolver().notifyChange(DocumentsContract
914            .buildRootsUri(AUTHORITY), null);
915}
916</pre>