page.title=Платформа доступа к хранилищу (Storage Access Framework) @jd:body

Содержание документа больше информации

  1. Обзор
  2. Поток управления
  3. Создание клиентского приложения
    1. Поиск документов
    2. Обработка результатов
    3. Изучение метаданных документа
    4. Открытие документа
    5. Создание нового документа
    6. Удаление документа
    7. Редактирование документа
    8. Удержание прав доступа
  4. Создание собственного поставщика документов
    1. Манифест
    2. Контракты
    3. Создание подкласса класса DocumentsProvider
    4. Безопасность

Ключевые классы

  1. {@link android.provider.DocumentsProvider}
  2. {@link android.provider.DocumentsContract}

Видео

  1. DevBytes: Android 4.4 Storage Access Framework: Поставщик
  2. DevBytes: Android 4.4 Storage Access Framework: Клиент

Примеры кода

  1. Класс StorageProvider
  2. Класс StorageClient

См. также:

  1. Основные сведения о поставщике контента

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

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

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

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

Обзор

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

data model

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

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

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

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

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

app

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

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

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

picker

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

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

picker

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

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

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

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

Намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT} не является заменой для намерения {@link android.content.Intent#ACTION_GET_CONTENT}. Разработчику следует использовать то, которое лучше соответствует потребностям приложения:

В этом разделе показано, как написать клиентское приложение, использующее намерения {@link android.content.Intent#ACTION_OPEN_DOCUMENT} и {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.

В следующем фрагменте кода намерение {@link android.content.Intent#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);
}

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

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

Когда пользователь выбирает документ в элементе выбора, вызывается метод {@link android.app.Activity#onActivityResult onActivityResult()}. Идентификатор URI, указывающий на выбранный документ, содержится в параметре{@code resultData}. Чтобы извлечь URI, следует вызвать {@link android.content.Intent#getData 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 документа, разработчик может открывать его и в целом делать с ним всё, что угодно.

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

Приведем пример кода для открытия объекта {@link android.graphics.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;
}

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

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

Далее приведен пример того, как можно получить объект {@link java.io.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();
}

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

Приложение может создать новый документ в поставщике документов, используя намерение {@link android.content.Intent#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 с помощью метода {@link android.app.Activity#onActivityResult onActivityResult()}, чтобы иметь возможность записывать в него данные.

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

Если у разработчика имеется URI документа, а объект {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} этого документа содержит флаг {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}, то документ можно удалить. Например:

DocumentsContract.deleteDocument(getContentResolver(), uri);

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

Платформа SAF позволяет редактировать текстовые документы на месте. В следующем фрагменте кода активизируется намерение {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, а категория {@link android.content.Intent#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);
}

Далее, из метода {@link android.app.Activity#onActivityResult onActivityResult()} (см. Обработка результатов) можно вызвать код для выполнения редактирования. В следующем фрагменте кода объект {@link java.io.FileOutputStream} получен с помощью объекта класса {@link android.content.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-идентификаторы, с которыми работало приложение. Однако не исключено, что они потеряют актуальность, поскольку другое приложение может удалить или модифицировать документ. Поэтому следует всегда вызывать {@code getContentResolver().takePersistableUriPermission()}, чтобы получать актуальные данные.

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

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

Манифест

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

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

<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 и ниже

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

Отключать фильтр намерения {@link android.content.Intent#ACTION_GET_CONTENT} на устройствах с Android версии 4.4 и выше рекомендуется следующим образом:

  1. В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values/}, добавить следующую строку:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. В файл ресурсов {@code bool.xml}, расположенный в каталоге {@code res/values-v19/}, добавить следующую строку:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Добавить псевдоним операции, чтобы отключить фильтр намерения {@link android.content.Intent#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>
    

Контракты

Как правило, при создании нестандартного поставщика контента одной из задач является реализация классов-контрактов, описанная в руководстве для разработчиков Поставщики контента. Класс-контракт представляет собой класс {@code 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

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

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

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

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

В следующем фрагменте кода параметр {@code 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

Реализация метода {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} должна возвращать объект{@link android.database.Cursor}, указывающий на все файлы в заданном каталоге, используя столбцы, определенные в {@link android.provider.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

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

Метод {@link android.provider.DocumentsProvider#queryDocument queryDocument()} возвращает ту же информацию, которую возвращал {@link android.provider.DocumentsProvider#queryChildDocuments 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

Необходимо реализовать метод {@link android.provider.DocumentsProvider#openDocument openDocument()}, который возвращает объект {@link android.os.ParcelFileDescriptor}, представляющий указанный файл. Другие приложения смогут воспользоваться возращенным объектом {@link android.os.ParcelFileDescriptor} для организации потока данных. Система вызывает этот метод, когда пользователь выбирает файл, и клиентское приложение запрашивает доступ нему, вызывая метод {@link android.content.ContentResolver#openFileDescriptor 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);
    }
}

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

Предположим, что поставщик документов представляет собой защищенную паролем службу хранения в облаке, а приложение должно убедиться, что пользователь вошел в систему, прежде чем оно предоставит ему доступ к файлам. Что должно предпринять приложение, если пользователь не выполнил вход? Решение состоит в том, чтобы реализация метода {@link android.provider.DocumentsProvider#queryRoots 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;
}

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

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