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>—поставщик контента, позволяющий 92службе хранения (например, Диск Google) показывать файлы, которыми он управляет. Поставщик документов 93реализуется как подкласс класса{@link android.provider.DocumentsProvider}. 94Его схема основана на традиционной файловой иерархии, 95однако физический способ хранения данных в поставщике документов остается на усмотрении разработчика. 96 Платформа Android включает в себя несколько встроенных поставщиков документов, таких как 97Загрузки, Изображения и Видео.</li> 98 99<li><strong>Клиентское приложение</strong>—пользовательское приложение, вызывающее намерение 100{@link android.content.Intent#ACTION_OPEN_DOCUMENT} и/или 101{@link android.content.Intent#ACTION_CREATE_DOCUMENT} и принимающее 102файлы, возвращаемые поставщиками документов.</li> 103 104<li><strong>Элемент выбора</strong>—системный пользовательский интерфейс, обеспечивающий пользователям доступ к документам у всех 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 "file chooser" 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 "opened", 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 "audio/ogg". 268 // To search for all documents available via installed storage providers, 269 // it would be "*/*". 270 intent.setType("image/*"); 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>@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 // "if there's anything to look at, look at it" conditionals. 335 if (cursor != null && cursor.moveToFirst()) { 336 337 // Note it's called "Display Name". 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, "Display Name: " + 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 "unpredictable". 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 = "Unknown"; 357 } 358 Log.i(TAG, "Size: " + 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 "opened", 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 "opened", 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("text/plain"); 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 & (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><provider></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>"true"</code>. 568 Необходимо экспортировать поставщик, чтобы он был виден другим приложениям.</li> 569 570<li>Атрибут <code>android:grantUriPermissions</code>, установленный в значение 571<code>"true"</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><bool name="atLeastKitKat">false</bool></pre></li> 586 587<li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить 588строчку <pre><bool name="atLeastKitKat">true</bool></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><manifest... > 599 ... 600 <uses-sdk 601 android:minSdkVersion="19" 602 android:targetSdkVersion="19" /> 603 .... 604 <provider 605 android:name="com.example.android.storageprovider.MyCloudProvider" 606 android:authorities="com.example.android.storageprovider.documents" 607 android:grantUriPermissions="true" 608 android:exported="true" 609 android:permission="android.permission.MANAGE_DOCUMENTS" 610 android:enabled="@bool/atLeastKitKat"> 611 <intent-filter> 612 <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> 613 </intent-filter> 614 </provider> 615 </application> 616 617</manifest></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><bool name="atMostJellyBeanMR2">true</bool></pre></li> 640 641<li>В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить 642следующую строку: <pre><bool name="atMostJellyBeanMR2">false</bool></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<!-- This activity alias is added so that GET_CONTENT intent-filter 651 can be disabled for builds on API level 19 and higher. --> 652<activity-alias android:name="com.android.example.app.MyPicker" 653 android:targetActivity="com.android.example.app.MyActivity" 654 ... 655 android:enabled="@bool/atMostJellyBeanMR2"> 656 <intent-filter> 657 <action android:name="android.intent.action.GET_CONTENT" /> 658 <category android:name="android.intent.category.OPENABLE" /> 659 <category android:name="android.intent.category.DEFAULT" /> 660 <data android:mimeType="image/*" /> 661 <data android:mimeType="video/*" /> 662 </intent-filter> 663</activity-alias> 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@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 "MyCloud". 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 "Recents" 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>@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>@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>@Override 844public ParcelFileDescriptor openDocument(final String documentId, 845 final String mode, 846 CancellationSignal signal) throws 847 FileNotFoundException { 848 Log.v(TAG, "openDocument, mode: " + 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 @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, "A file with id " + 869 documentId + " has been closed! 870 Time to " + 871 "update the server."); 872 } 873 874 }); 875 } catch (IOException e) { 876 throw new FileNotFoundException("Failed to open document with id " 877 + documentId + " and mode " + 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>