lightbulb_outline Help shape the future of the Google Play Console, Android Studio, and Firebase. Start survey

Платформа доступа к хранилищу (Storage Access Framework)

Платформа доступа к хранилищу (Storage Access Framework, SAF) впервые появилась в Android версии 4.4 (API уровня 19). Платформа SAF облегчает пользователям поиск и открытие документов, изображений и других файлов в хранилищах всех поставщиков, с которыми они работают. Стандартный удобный интерфейс позволяет пользователям применять единый для всех приложений и поставщиков способ поиска файлов и доступа к последним добавленным файлам.

Облачные или локальные службы хранения могут присоединиться к этой экосистеме, реализовав класс DocumentsProvider, инкапсулирующий их услуги. Клиентские приложения, которым требуется доступ к документам поставщика, могут интегрироваться с SAF с помощью всего нескольких строчек кода.

Платформа SAF включает в себя следующие компоненты:

  • Поставщик документов—поставщик контента, позволяющий службе хранения (например, Диск Google) показывать файлы, которыми он управляет. Поставщик документов реализуется как подкласс классаDocumentsProvider. Его схема основана на традиционной файловой иерархии, однако физический способ хранения данных в поставщике документов остается на усмотрении разработчика. Платформа Android включает в себя несколько встроенных поставщиков документов, таких как Загрузки, Изображения и Видео.
  • Клиентское приложение—пользовательское приложение, вызывающее намерение ACTION_OPEN_DOCUMENT и/или ACTION_CREATE_DOCUMENT и принимающее файлы, возвращаемые поставщиками документов.
  • Элемент выбора—системный пользовательский интерфейс, обеспечивающий пользователям доступ к документам у всех поставщиков документов, которые удовлетворяют критериям поиска, заданным в клиентском приложении.

Платформа SAF в числе прочих предоставляет следующие функции:

  • позволяет пользователям искать контент у всех поставщиков документов, а не только у одного приложения;
  • обеспечивает приложению возможность долговременного, постоянного доступа к документам, принадлежащим поставщику документов. Благодаря такому доступу пользователи могут добавлять, редактировать, сохранять и удалять файлы, хранящиеся у поставщика;
  • поддерживает несколько учетных записей и временные корневые каталоги, например, поставщики на USB-накопителях, которые появляются, только когда накопитель вставлен в порт.

Обзор

В центре платформы SAF находится поставщик контента, являющийся подклассом класса DocumentsProvider. Внутри поставщика документовданные имеют структуру традиционной файловой иерархии:

data model

Рисунок 1. Модель данных поставщика документов. На рисунке Root (Корневой каталог) указывает на один объект Document (Документ), который затем разветвляется в целое дерево.

Обратите внимание на следующее.

  • Каждый поставщик документов предоставляет один или несколько «корневых каталогов», являющихся отправными точками при обходе дерева документов. Каждый корневой каталог имеет уникальный идентификатор COLUMN_ROOT_ID и указывает на документ (каталог), представляющий содержимое на уровне ниже корневого. Корневые каталоги динамичны по своей конструкции, чтобы обеспечивать поддержку таким вариантам использования, как несколько учетных записей, временные хранилища на USB-нкопителях и возможность для пользователя войти в систему и выйти из нее.
  • В каждом корневом каталоге находится один документ. Этот документ указывает на количество документов N каждый из которых, в свою очередь, может указывать на один или N документов.
  • Каждый сервер хранилища показывает отдельные файлы и каталоги, ссылаясь на них с помощью уникального идентификатора COLUMN_DOCUMENT_ID. Идентификаторы документов должны быть уникальными и не меняться после присвоения, поскольку они используются для выдачи постоянных URI, не зависящих от перезагрузки устройства.
  • Документ — это или открываемый файл (имеющий конкретный MIME-тип), или каталог, содержащий другие документы (с MIME-типом MIME_TYPE_DIR).
  • Каждый документ может иметь различные свойства, описываемые флагами COLUMN_FLAGS, такими какFLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE и FLAG_SUPPORTS_THUMBNAIL. Документ с одним и тем же идентификатором COLUMN_DOCUMENT_ID может находиться в нескольких каталогах.

Поток управления

Как было сказано выше, модель данных поставщика документов основана на традиционной файловой иерархии. Однако физический способ хранения данных остается на усмотрение разработчика, при условии, что к ним можно обращаться через API-интерфейс DocumentsProvider. Например, можно использовать для данных облачное хранилище на основе тегов.

На рисунке 2 показан пример того, как приложение для обработки фотографий может использовать SAF для доступа к сохраненным данным:

app

Рисунок 2. Поток управления Storage Access Framework

Обратите внимание на следующее.

  • На платформе SAF поставщики и клиенты не взаимодействуют напрямую. Клиент запрашивает разрешение на взаимодействие с файлами (то есть, на чтение, редактирование, создание или удаление файлов).
  • Взаимодействие начинается, когда приложение (в нашем примере обрабатывающее фотографии) активизирует намерение ACTION_OPEN_DOCUMENT или ACTION_CREATE_DOCUMENT. Намерение может включать в себя фильтры для уточнения критериев, например, «предоставить открываемые файлы с MIME-типом image».
  • Когда намерение срабатывает, системный элемент выбора переходит к каждому зарегистрированному поставщику и показывает пользователю корневые каталоги с контентом, соответствующим запросу.
  • Элемент выбора предоставляет пользователю стандартный интерфейс, даже если поставщики документов значительно различаются. В качестве примера на рисунке 2 изображены Диск Google, поставщик на USB-накопителе и облачный поставщик.

На рисунке 3 показан элемент выбора, в котором пользователь для поиска изображений выбрал учетную запись Диск Google:

picker

Рисунок 3. Элемент выбора

Когда пользователь выбирает Диск Google, изображения отображаются, как показано на рисунке 4. С этого момента пользователь может взаимодействовать с ними любыми способами, которые поддерживаются поставщиком и клиентским приложением.

picker

Рисунок 4. Изображения

Создание клиентского приложения

В Android версии 4.3 и ниже для того, чтобы приложение могло получать файл от другого приложения, оно должно активизировать намерение, например, ACTION_PICK или ACTION_GET_CONTENT. После этого пользователь должен выбрать какое-либо одно приложение, чтобы получить файл, а оно должно предоставить пользователю интерфейс, с помощью которого он сможет выбирать и получать файлы.

Начиная с Android 4.4 и выше, у разработчика имеется дополнительная возможность — намерение ACTION_OPEN_DOCUMENT, которое отображает пользовательский интерфейс элемента выбора, управляемого системой. Этот элемент предоставляет пользователю обзор всех файлов, доступных в других приложениях. Благодаря этому единому интерфейсу, пользователь может выбрать файл в любом из поддерживаемых приложений.

Намерение ACTION_OPEN_DOCUMENT не является заменой для намерения ACTION_GET_CONTENT. Разработчику следует использовать то, которое лучше соответствует потребностям приложения:

  • используйте ACTION_GET_CONTENT, если приложению нужно просто прочитать или импортировать данные. При таком подходе приложение импортирует копию данных, например, файл с изображением.
  • используйте ACTION_OPEN_DOCUMENT, если приложению нужна возможность долговременного, постоянного доступа к документам, принадлежащим поставщику документов. В качестве примера можно назвать редактор фотографий, позволяющий пользователям обрабатывать изображения, хранящиеся в поставщике документов.

В этом разделе показано, как написать клиентское приложение, использующее намерения ACTION_OPEN_DOCUMENT и ACTION_CREATE_DOCUMENT.

В следующем фрагменте кода намерение ACTION_OPEN_DOCUMENT используется для поиска поставщиков документов, содержащих файлы изображений:

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);
}

Обратите внимание на следующее.

  • Когда приложение активизирует намерение ACTION_OPEN_DOCUMENT , оно запускает элемент выбора, отображающий всех поставщиков документов, соответствующих заданным критериям.
  • Добавление категории CATEGORY_OPENABLE в фильтры намерения приводит к отображению только тех документов, которые можно открыть, например, файлов с изображениями.
  • Оператор intent.setType("image/*") выполняет дальнейшую фильтрацию, чтобы отображались только документы с MIME-типом image.

Обработка результатов

Когда пользователь выбирает документ в элементе выбора, вызывается метод onActivityResult(). Идентификатор URI, указывающий на выбранный документ, содержится в параметреresultData. Чтобы извлечь URI, следует вызвать getData(). Этот URI можно использовать для получения документа, нужного пользователю. Например:

@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);
        }
    }
}

Изучение метаданных документа

Имея в своем распоряжении URI документа, разработчик получает доступ к его метаданным. В следующем фрагменте кода метаданные документа, определяемого идентификатором URI, считываются и записываются в журнал:

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();
    }
}

Открытие документа

Получив URI документа, разработчик может открывать его и в целом делать с ним всё, что угодно.

Объект растровых изображений

Приведем пример кода для открытия объекта Bitmap:

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;
}

Обратите внимание, что не следует производить эту операцию в потоке пользовательского интерфейса. Ее нужно выполнять в фоне, с помощью AsyncTask. Когда файл с растровым изображением откроется, его можно отобразить в виджете ImageView.

Получение объекта InputStream

Далее приведен пример того, как можно получить объект InputStream по идентификатору URI. В этом фрагменте кода строчки файла считываются в объект строкового типа:

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();
}

Создание нового документа

Приложение может создать новый документ в поставщике документов, используя намерение ACTION_CREATE_DOCUMENT . Чтобы создать файл, нужно указать в намерении MIME-тип и имя файла, а затем запустить его с уникальным кодом запроса. Об остальном позаботится платформа:

// 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);
}

После создания нового документа можно получить его URI с помощью метода onActivityResult(), чтобы иметь возможность записывать в него данные.

Удаление документа

Если у разработчика имеется URI документа, а объект Document.COLUMN_FLAGS этого документа содержит флаг SUPPORTS_DELETE, то документ можно удалить. Например:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Редактирование документа

Платформа SAF позволяет редактировать текстовые документы на месте. В следующем фрагменте кода активизируется намерение ACTION_OPEN_DOCUMENT, а категория CATEGORY_OPENABLE используется, чтобы отображались только документы, которые можно открыть. Затем производится дальнейшая фильтрация, чтобы отображались только текстовые файлы:

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);
}

Далее, из метода onActivityResult() (см. Обработка результатов) можно вызвать код для выполнения редактирования. В следующем фрагменте кода объект FileOutputStream получен с помощью объекта класса ContentResolver. По умолчанию используется режим записи. Рекомендуется запрашивать минимально необходимые права доступа, поэтому не следует запрашивать чтение/запись, если приложению требуется только записать файл:

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();
    }
}

Удержание прав доступа

Когда приложение открывает файл для чтения или записи, система предоставляет ему URI-разрешение на этот файл. Разрешение действует вплоть до перезагрузки устройства. Предположим, что в графическом редакторе требуется, чтобы у пользователя была возможность открыть непосредственно в этом приложении последние пять изображений, которые он редактировал. Если он перезапустил устройство, возникает необходимость снова отсылать его к системному элементу выбора для поиска файлов. Очевидно, это далеко не идеальный вариант.

Чтобы избежать такой ситуации, разработчик может удержать права доступа, предоставленные системой его приложению. Приложение фактически принимает постоянное URI-разрешение, предлагаемое системой. В результате пользователь получает непрерывный доступ к файлам из приложения, независимо от перезагрузки устройства:

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);

Остается один заключительный шаг. Можно сохранить последние URI-идентификаторы, с которыми работало приложение. Однако не исключено, что они потеряют актуальность, поскольку другое приложение может удалить или модифицировать документ. Поэтому следует всегда вызывать getContentResolver().takePersistableUriPermission(), чтобы получать актуальные данные.

Создание собственного поставщика документов

При разработке приложения, оказывающего услуги по хранению файлов (например, службы хранения в облаке), можно предоставить доступ к файлам при помощи SAF, написав собственный поставщик документов. В этом разделе показано, как это сделать.

Манифест

Чтобы реализовать собственный поставщик документов, необходимо добавить в манифест приложения следующую информацию:

  • Целевой API-интерфейс уровня 19 или выше.
  • Элемент <provider>, в котором объявляется нестандартный поставщик хранилища.
  • Имя поставщика, т. е., имя его класса с именем пакета. Например: com.example.android.storageprovider.MyCloudProvider.
  • Имя центра поставщика, т. е. имя пакета (в этом примере — com.example.android.storageprovider) с типом поставщика контента (documents). Например,com.example.android.storageprovider.documents.
  • Атрибут android:exported, установленный в значение "true". Необходимо экспортировать поставщик, чтобы он был виден другим приложениям.
  • Атрибут android:grantUriPermissions, установленный в значение "true". Этот параметр позволяет системе предоставлять другим приложениям доступ к контенту поставщика. Обсуждение того, как следует удерживать права доступа к конкретному документу см. в разделе Удержание прав доступа.
  • Разрешение MANAGE_DOCUMENTS. По умолчанию поставщик доступен всем. Добавление этого разрешения в манифест делает поставщик доступным только системе. Это важно для обеспечения безопасности.
  • Атрибут android:enabled, имеющий логическое значение, определенное в файле ресурсов. Этот атрибут предназначен для отключения поставщика на устройствах под управлением Android версии 4.3 и ниже. Например: android:enabled="@bool/atLeastKitKat". Помимо включения этого атрибута в манифест, необходимо сделать следующее:
    • В файл ресурсов bool.xml, расположенный в каталоге res/values/, добавить строчку
      <bool name="atLeastKitKat">false</bool>
    • В файл ресурсов bool.xml, расположенный в каталоге res/values-v19/, добавить строчку
      <bool name="atLeastKitKat">true</bool>
  • Фильтр намерения с действием android.content.action.DOCUMENTS_PROVIDER, чтобы поставщик появлялся в элементе выбора, когда система будет искать поставщиков.

Ниже приведены отрывки из образца манифеста, включающего в себя поставщик:

<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>

Поддержка устройств под управлением Android версии 4.3 и ниже

Намерение ACTION_OPEN_DOCUMENT доступно только на устройствах с Android версии 4.4 и выше. Если приложение должно поддерживать ACTION_GET_CONTENT, чтобы обслуживать устройства, работающие под управлением Android 4.3 и ниже, необходимо отключить фильтр намерения ACTION_GET_CONTENT в манифесте для устройств с Android версии 4.4 и выше. Поставщик документов и намерение ACTION_GET_CONTENT следует считать взаимоисключающими. Если приложение поддерживает их одновременно, оно будет появляться в пользовательском интерфейсе системного элемента выбора дважды, предлагая два различных способа доступа к сохраненным данным. Это запутает пользователей.

Отключать фильтр намерения ACTION_GET_CONTENT на устройствах с Android версии 4.4 и выше рекомендуется следующим образом:

  1. В файл ресурсов bool.xml, расположенный в каталоге res/values/, добавить следующую строку:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. В файл ресурсов bool.xml, расположенный в каталоге res/values-v19/, добавить следующую строку:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Добавить псевдоним операции, чтобы отключить фильтр намерения ACTION_GET_CONTENT для версий 4.4 (API уровня 19) и выше. Например:
    <!-- 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>
    

Контракты

Как правило, при создании нестандартного поставщика контента одной из задач является реализация классов-контрактов, описанная в руководстве для разработчиков Поставщики контента. Класс-контракт представляет собой класс public final, в котором содержатся определения констант для URI, имен столбцов, типов MIME и других метаданных поставщика. Платформа SAF предоставляет разработчику следующие классы-контракты, так что ему не нужно писать собственные:

Например, когда к поставщику документов приходит запрос на документы или корневой каталог, можно возвращать в курсоре следующие столбцы:

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,};

Создание подкласса класса DocumentsProvider

Следующим шагом в разработке собственного поставщика документов является создание подкласса абстрактного класса DocumentsProvider. Как минимум, необходимо реализовать следующие методы:

Это единственные методы, реализация которых строго обязательна, однако существует намного больше методов, которые, возможно, тоже придется реализовать. Подробности приводятся в описании классаDocumentsProvider .

Реализация метода queryRoots

Реализация метода queryRoots() должна возвращать объект Cursor, указывающий на все корневые каталоги поставщиков документов, используя столбцы, определенные в DocumentsContract.Root.

В следующем фрагменте кода параметр projection представляет конкретные поля, нужные вызывающему объекту. В этом коде создается курсор, и к нему добавляется одна строка, соответствующая одному корневому каталогу (каталогу верхнего уровня), например, Загрузки или Изображения. Большинство поставщиков имеет только один корневой каталог. Однако ничто не мешает иметь несколько корневых каталогов, например, при наличии нескольких учетных записей. В этом случае достаточно добавить в курсор еще одну строку.

@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;
}

Реализация метода queryChildDocuments

Реализация метода queryChildDocuments() должна возвращать объектCursor, указывающий на все файлы в заданном каталоге, используя столбцы, определенные в DocumentsContract.Document.

Этот метод вызывается, когда в интерфейсе элемента выбора пользователь выбирает корневой каталог приложения. Метод получает документы-потомки каталога на уровне ниже корневого. Его можно вызывать на любом уровне файловой иерархии, а не только в корневом каталоге. В следующем фрагменте кода создается курсор с запрошенными столбцами. Затем в него заносится информация о каждом ближайшем потомке родительского каталога. Потомком может быть изображение, еще один каталог, в общем, любой файл:

@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;
}

Реализация метода queryDocument

Реализация метода queryDocument() должна возвращать объектCursor, указывающий на заданный файл, используя столбцы, определенные вDocumentsContract.Document.

Метод queryDocument() возвращает ту же информацию, которую возвращал queryChildDocuments(), но для конкретного файла:

@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;
}

Реализация метода openDocument

Необходимо реализовать метод openDocument(), который возвращает объект ParcelFileDescriptor, представляющий указанный файл. Другие приложения смогут воспользоваться возращенным объектом ParcelFileDescriptor для организации потока данных. Система вызывает этот метод, когда пользователь выбирает файл, и клиентское приложение запрашивает доступ нему, вызывая метод openFileDescriptor(). Например:

@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);
    }
}

Безопасность

Предположим, что поставщик документов представляет собой защищенную паролем службу хранения в облаке, а приложение должно убедиться, что пользователь вошел в систему, прежде чем оно предоставит ему доступ к файлам. Что должно предпринять приложение, если пользователь не выполнил вход? Решение состоит в том, чтобы реализация метода queryRoots() не возвращала корневых каталогов. Иными словами, это должен быть пустой корневой курсор:

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;
}

Следующий шаг состоит в вызове метода getContentResolver().notifyChange(). Помните объект DocumentsContract? Воспользуемся им для создания соответствующего URI. В следующем фрагменте кода система извещается о необходимости опрашивать корневые каталоги поставщика документов, когда меняется статус входа пользователя в систему. Если пользователь не выполнил вход, метод queryRoots() возвратит пустой курсор, как показано выше. Это гарантирует, что документы поставщика будут доступны только пользователям, вошедшим в поставщик.

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}