カスタム ドキュメント プロバイダを作成する

ファイル用のストレージ サービスを提供するアプリ(クラウド保存サービスなど)を開発する場合は、カスタム ドキュメント プロバイダを記述することで、ストレージ アクセス フレームワーク(SAF)を通じてファイルを使用可能にできます。このページでは、カスタム ドキュメント プロバイダの作成方法について説明します。

ストレージ アクセス フレームワークの詳細については、ストレージ アクセス フレームワークの概要をご覧ください。

マニフェスト

カスタム ドキュメント プロバイダを実装するには、アプリケーションのマニフェストに次の行を追加します。

  • API レベル 19 以降を対象にする。
  • カスタム ストレージ プロバイダを宣言する <provider> 要素。
  • DocumentsProvider サブクラスの名前(パッケージ名を含むクラス名)に設定された属性 android:name:

    com.example.android.storageprovider.MyCloudProvider

  • android:authority 属性。パッケージ名(この例では com.example.android.storageprovider)とコンテンツ プロバイダのタイプ(documents)を指定します。
  • "true" に設定された属性 android:exported。プロバイダをエクスポートして、他のアプリで表示できるようにする必要があります。
  • "true" に設定された属性 android:grantUriPermissions。この設定により、システムは他のアプリにプロバイダのコンテンツへのアクセスを許可できます。このような他のアプリがプロバイダのコンテンツへのアクセスを維持する方法については、権限を保持するをご覧ください。
  • MANAGE_DOCUMENTS 権限。デフォルトでは、プロバイダはすべてのユーザーが使用できます。この権限を追加すると、プロバイダはシステムに制限されます。セキュリティの面で、この制限は重要です。
  • 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">
            <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 以前を搭載しているデバイスに対応させたい場合は、Android 4.4 以上を搭載したデバイスのマニフェストで ACTION_GET_CONTENT インテント フィルタを無効にする必要があります。ドキュメント プロバイダと ACTION_GET_CONTENT は相互に排他的であるとみなす必要があります。両方を同時にサポートしている場合、アプリはシステム選択 UI に 2 回表示され、保存されたデータにアクセスする 2 つの異なる方法を提示します。これはユーザーの混乱の元になります。

Android バージョン 4.4 以降を搭載しているデバイスで ACTION_GET_CONTENT インテント フィルタを無効にするおすすめの方法は次のとおりです。

  1. res/values/ の下の bool.xml リソース ファイルで、次の行を追加します。
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ の下の bool.xml リソース ファイルで、次の行を追加します。
    <bool name="atMostJellyBeanMR2">false</bool>
  3. アクティビティ エイリアスを追加して、バージョン 4.4(API レベル 19)以降で ACTION_GET_CONTENT インテント フィルタを無効にします。次に例を示します。
    <!-- 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>
    

契約

通常、カスタム コンテンツ プロバイダを記述する場合、 コンテンツ プロバイダのデベロッパー ガイドで説明されているように、タスクの 1 つとしてコントラクト クラスの実装があります。コントラクト クラスは、URI、列名、MIME タイプ、プロバイダに関連するその他のメタデータの定数定義を含む public final クラスです。SAF は次のコントラクト クラスを提供するため、独自に記述する必要はありません。

たとえば、ドキュメント プロバイダでドキュメントまたはルートをクエリしたときにカーソルで返される列は次のとおりです。

Kotlin

private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Root.COLUMN_ROOT_ID,
        DocumentsContract.Root.COLUMN_MIME_TYPES,
        DocumentsContract.Root.COLUMN_FLAGS,
        DocumentsContract.Root.COLUMN_ICON,
        DocumentsContract.Root.COLUMN_TITLE,
        DocumentsContract.Root.COLUMN_SUMMARY,
        DocumentsContract.Root.COLUMN_DOCUMENT_ID,
        DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Document.COLUMN_DOCUMENT_ID,
        DocumentsContract.Document.COLUMN_MIME_TYPE,
        DocumentsContract.Document.COLUMN_DISPLAY_NAME,
        DocumentsContract.Document.COLUMN_LAST_MODIFIED,
        DocumentsContract.Document.COLUMN_FLAGS,
        DocumentsContract.Document.COLUMN_SIZE
)

Java

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() の実装では、DocumentsContract.Root で定義された列を使用して、ドキュメント プロバイダのすべてのルート ディレクトリを指す Cursor を返す必要があります。

次のスニペットでは、projection パラメータは呼び出し元が取得したい特定のフィールドを表しています。スニペットは新しいカーソルを作成して、そこに 1 行を追加します(1 つのルート、最上位ディレクトリ(ダウンロードやイメージなど))。ほとんどのプロバイダにはルートが 1 つしかありませんが、複数のユーザー アカウントなど、複数のアカウントを持つ場合があります。その場合は、カーソルに 2 番目の行を追加するだけです。

Kotlin

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = 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.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.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.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

        // The child MIME types are used to filter the roots and only present to the
        // user those roots that contain the desired type somewhere in their file hierarchy.
        add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir))
        add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
        add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher)
    }

    return result
}

Java

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Use a MatrixCursor to build 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.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    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 after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

    // The child MIME types are used to filter the roots and only present to the
    // user those roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

ドキュメント プロバイダが動的なルートセット(切断される USB デバイスやユーザーがログアウトできるアカウントなど)に接続する場合は、次のコード スニペットに示すように、ContentResolver.notifyChange() メソッドを使用してドキュメント UI を更新し、変更との同期を維持できます。

Kotlin

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

Java

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

プロバイダ内のドキュメントを一覧表示する

queryChildDocuments() の実装では、DocumentsContract.Document で定義された列を使用して、指定されたディレクトリ内のすべてのファイルを指す Cursor を返す必要があります。

このメソッドは、ユーザーが選択ツール UI でルートを選択すると呼び出されます。このメソッドは、COLUMN_DOCUMENT_ID で指定されたドキュメント ID の子を取得します。ユーザーがドキュメント プロバイダ内のサブディレクトリを選択するたびに、このメソッドが呼び出されます。

このスニペットは、リクエストされた列を持つ新しいカーソルを作成し、親ディレクトリ内のすべての直接の子に関する情報をカーソルに追加します。 子は、画像、別のディレクトリ、任意のファイルです。

Kotlin

override fun queryChildDocuments(
        parentDocumentId: String?,
        projection: Array<out String>?,
        sortOrder: String?
): Cursor {
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        val parent: File = getFileForDocId(parentDocumentId)
        parent.listFiles()
                .forEach { file ->
                    includeFile(this, null, file)
                }
    }
}

Java

@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() の実装では、DocumentsContract.Document で定義された列を使用して、指定されたファイルを指す Cursor を返す必要があります。

queryDocument() メソッドは、queryChildDocuments() で渡されたものと同じ情報を返しますが、対象は特定のファイルです。

Kotlin

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // Create a cursor with the requested projection, or the default projection.
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        includeFile(this, documentId, null)
    }
}

Java

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

ドキュメント プロバイダは、DocumentsProvider.openDocumentThumbnail() メソッドをオーバーライドして、サポートされているファイルに FLAG_SUPPORTS_THUMBNAIL フラグを追加することで、ドキュメントのサムネイルを指定することもできます。次のコード スニペットは、DocumentsProvider.openDocumentThumbnail() の実装方法の一例を示しています。

Kotlin

override fun openDocumentThumbnail(
        documentId: String?,
        sizeHint: Point?,
        signal: CancellationSignal?
): AssetFileDescriptor {
    val file = getThumbnailFileForDocId(documentId)
    val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}

Java

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
                                                     CancellationSignal signal)
        throws FileNotFoundException {

    final File file = getThumbnailFileForDocId(documentId);
    final ParcelFileDescriptor pfd =
        ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}

注意: ドキュメント プロバイダは、sizeHint パラメータで指定されたサイズの 2 倍を超えるサムネイル画像を返さないでください。

ドキュメントを開く

指定されたファイルを表す ParcelFileDescriptor を返すように openDocument() を実装する必要があります。他のアプリは、返された ParcelFileDescriptor を使用してデータをストリーミングできます。ユーザーがファイルを選択すると、システムによってこのメソッドが呼び出され、クライアント アプリが openFileDescriptor() を呼び出してファイルへのアクセスをリクエストします。次に例を示します。

Kotlin

override fun openDocument(
        documentId: String,
        mode: String,
        signal: CancellationSignal
): ParcelFileDescriptor {
    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).

    val file: File = getFileForDocId(documentId)
    val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

    val isWrite: Boolean = mode.contains("w")
    return if (isWrite) {
        val handler = Handler(context.mainLooper)
        // Attach a close listener if the document is opened in write mode.
        try {
            ParcelFileDescriptor.open(file, accessMode, handler) {
                // 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 (e: IOException) {
            throw FileNotFoundException(
                    "Failed to open document with id $documentId and mode $mode"
            )
        }
    } else {
        ParcelFileDescriptor.open(file, accessMode)
    }
}

Java

@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 int accessMode = ParcelFileDescriptor.parseMode(mode);

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

ドキュメント プロバイダがファイルをストリーミングしたり、複雑なデータ構造を処理したりする場合は、createReliablePipe() メソッドまたは createReliableSocketPair() メソッドの実装を検討してください。これらのメソッドを使用すると、ParcelFileDescriptor オブジェクトのペアを作成できます。一方のオブジェクトを返し、他方を ParcelFileDescriptor.AutoCloseOutputStream または ParcelFileDescriptor.AutoCloseInputStream を介して送信できます。

最新のドキュメントと検索をサポートする

queryRecentDocuments() メソッドをオーバーライドして FLAG_SUPPORTS_RECENTS を返すと、ドキュメント プロバイダのルートの下にある最近変更されたドキュメントのリストを指定できます。次のコード スニペットは、queryRecentDocuments() メソッドの実装方法の例を示しています。

Kotlin

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    val result = MatrixCursor(resolveDocumentProjection(projection))

    val parent: File = getFileForDocId(rootId)

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    val lastModifiedFiles = PriorityQueue(
            5,
            Comparator<File> { i, j ->
                Long.compare(i.lastModified(), j.lastModified())
            }
    )

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    val pending : MutableList<File> = mutableListOf()

    // Start by adding the parent to the list of files to be processed
    pending.add(parent)

    // Do while we still have unexamined files
    while (pending.isNotEmpty()) {
        // Take a file from the list of unprocessed files
        val file: File = pending.removeAt(0)
        if (file.isDirectory) {
            // If it's a directory, add all its children to the unprocessed list
            pending += file.listFiles()
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file)
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
        val file: File = lastModifiedFiles.remove()
        includeFile(result, null, file)
    }
    return result
}

Java

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
        throws FileNotFoundException {

    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result =
        new MatrixCursor(resolveDocumentProjection(projection));

    final File parent = getFileForDocId(rootId);

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    PriorityQueue lastModifiedFiles =
        new PriorityQueue(5, new Comparator() {

        public int compare(File i, File j) {
            return Long.compare(i.lastModified(), j.lastModified());
        }
    });

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    final LinkedList pending = new LinkedList();

    // Start by adding the parent to the list of files to be processed
    pending.add(parent);

    // Do while we still have unexamined files
    while (!pending.isEmpty()) {
        // Take a file from the list of unprocessed files
        final File file = pending.removeFirst();
        if (file.isDirectory()) {
            // If it's a directory, add all its children to the unprocessed list
            Collections.addAll(pending, file.listFiles());
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file);
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
        final File file = lastModifiedFiles.remove();
        includeFile(result, null, file);
    }
    return result;
}

StorageProvider コードサンプルをダウンロードすると、上記のスニペットの完全なコードを取得できます。

ドキュメントの作成をサポートする

クライアント アプリは、ドキュメント プロバイダ内でのファイルの作成を許可できます。クライアント アプリが ACTION_CREATE_DOCUMENT インテントを送信した場合、ドキュメント プロバイダは、そのクライアント アプリでドキュメント プロバイダ内で新しいドキュメントを作成することを許可できます。

ドキュメントの作成をサポートするには、ルートに FLAG_SUPPORTS_CREATE フラグが必要です。ディレクトリ内に新しいファイルを作成できるようにするディレクトリには、FLAG_DIR_SUPPORTS_CREATE フラグが必要です。

また、ドキュメント プロバイダが createDocument() メソッドを実装する必要もあります。ユーザーがドキュメント プロバイダ内のディレクトリを選択して新しいファイルを保存すると、ドキュメント プロバイダは createDocument() に対する呼び出しを受け取ります。createDocument() メソッドの実装内で、ファイルの新しい COLUMN_DOCUMENT_ID を返します。クライアント アプリは、この ID を使用してファイルのハンドルを取得し、最終的に openDocument() を呼び出して新しいファイルに書き込むことができます。

次のコード スニペットは、ドキュメント プロバイダ内に新しいファイルを作成する方法を示しています。

Kotlin

override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String {
    val parent: File = getFileForDocId(documentId)
    val file: File = try {
        File(parent.path, displayName).apply {
            createNewFile()
            setWritable(true)
            setReadable(true)
        }
    } catch (e: IOException) {
        throw FileNotFoundException(
                "Failed to create document with name $displayName and documentId $documentId"
        )
    }

    return getDocIdForFile(file)
}

Java

@Override
public String createDocument(String documentId, String mimeType, String displayName)
        throws FileNotFoundException {

    File parent = getFileForDocId(documentId);
    File file = new File(parent.getPath(), displayName);
    try {
        file.createNewFile();
        file.setWritable(true);
        file.setReadable(true);
    } catch (IOException e) {
        throw new FileNotFoundException("Failed to create document with name " +
                displayName +" and documentId " + documentId);
    }
    return getDocIdForFile(file);
}

StorageProvider コードサンプルをダウンロードすると、上記のスニペットの完全なコードを取得できます。

ドキュメント管理機能をサポートする

ドキュメント プロバイダは、ファイルを開く、作成する、表示するだけでなく、クライアント アプリでファイルの名前変更、コピー、移動、削除を行うこともできます。ドキュメント プロバイダにドキュメント管理機能を追加するには、ドキュメントの COLUMN_FLAGS 列にフラグを追加して、サポートされている機能を示します。また、DocumentsProvider クラスの対応するメソッドを実装する必要があります。

次の表に、ドキュメント プロバイダが特定の機能を公開するために実装する必要がある COLUMN_FLAGS フラグと DocumentsProvider メソッドを示します。

機能 フラグ メソッド
ファイルを削除する FLAG_SUPPORTS_DELETE deleteDocument()
ファイル名を変更する FLAG_SUPPORTS_RENAME renameDocument()
ドキュメント プロバイダ内の新しい親ディレクトリにファイルをコピーする FLAG_SUPPORTS_COPY copyDocument()
ドキュメント プロバイダ内のディレクトリ間でファイルを移動する FLAG_SUPPORTS_MOVE moveDocument()
親ディレクトリからファイルを削除する FLAG_SUPPORTS_REMOVE removeDocument()

仮想ファイル形式と代替ファイル形式をサポートする

仮想ファイルは、Android 7.0(API レベル 24)で導入された機能です。ドキュメント プロバイダは、直接的なバイトコード表現を持たないファイルに対する閲覧アクセスを提供できます。他のアプリが仮想ファイルを表示できるようにするには、ドキュメント プロバイダが仮想ファイル用に別のオープン可能なファイル表現を生成する必要があります。

たとえば、他のアプリが直接開けないファイル形式、つまり仮想ファイルがドキュメント プロバイダに含まれているとします。クライアント アプリが CATEGORY_OPENABLE カテゴリを指定せずに ACTION_VIEW インテントを送信すると、ユーザーはドキュメント プロバイダ内の仮想ファイルを選択して表示できます。その後、ドキュメント プロバイダは仮想ファイルを、異なるがオープン可能なファイル形式(画像など)で返します。これにより、クライアント アプリがその仮想ファイルを開いて、ユーザーに提示できるようになります。

プロバイダ内のドキュメントが仮想であることを宣言するには、queryDocument() メソッドから返されるファイルに FLAG_VIRTUAL_DOCUMENT フラグを追加する必要があります。このフラグは、ファイルが直接のバイトコード表現を持たず、直接開けないことを、クライアント アプリに通知します。

ドキュメント プロバイダ内のファイルが仮想であることを宣言している場合は、画像や PDF などの別の MIME タイプで使用できるようにすることを強くおすすめします。ドキュメント プロバイダは、getDocumentStreamTypes() メソッドをオーバーライドして、仮想ファイルの表示でサポートされている代替 MIME タイプを宣言します。クライアント アプリが getStreamTypes(android.net.Uri, java.lang.String) メソッドを呼び出すと、システムによってドキュメント プロバイダの getDocumentStreamTypes() メソッドが呼び出されます。すると、getDocumentStreamTypes() メソッドは、ドキュメント プロバイダがファイルに対してサポートしている代替 MIME タイプの配列を返します。

ドキュメント プロバイダが表示可能なファイル形式でドキュメントを生成できるとクライアントが判断すると、クライアント アプリは openTypedAssetFileDescriptor() メソッドを呼び出します。このメソッドはドキュメント プロバイダの openTypedDocument() メソッドを内部で呼び出します。ドキュメント プロバイダは、リクエストされたファイル形式でクライアント アプリにファイルを返します。

次のコード スニペットは、getDocumentStreamTypes() メソッドと openTypedDocument() メソッドの単純な実装を示しています。

Kotlin

var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg")
override fun openTypedDocument(
        documentId: String?,
        mimeTypeFilter: String,
        opts: Bundle?,
        signal: CancellationSignal?
): AssetFileDescriptor? {
    return try {
        // Determine which supported MIME type the client app requested.
        when(mimeTypeFilter) {
            "image/jpg" -> openJpgDocument(documentId)
            "image/png", "image/*", "*/*" -> openPngDocument(documentId)
            else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter")
        }
    } catch (ex: Exception) {
        Log.e(TAG, ex.message)
        null
    }
}

override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> {
    return when (mimeTypeFilter) {
        "*/*", "image/*" -> {
            // Return all supported MIME types if the client app
            // passes in '*/*' or 'image/*'.
            SUPPORTED_MIME_TYPES
        }
        else -> {
            // Filter the list of supported mime types to find a match.
            SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray()
        }
    }
}

Java


public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"};

@Override
public AssetFileDescriptor openTypedDocument(String documentId,
    String mimeTypeFilter,
    Bundle opts,
    CancellationSignal signal) {

    try {

        // Determine which supported MIME type the client app requested.
        if ("image/png".equals(mimeTypeFilter) ||
            "image/*".equals(mimeTypeFilter) ||
            "*/*".equals(mimeTypeFilter)) {

            // Return the file in the specified format.
            return openPngDocument(documentId);

        } else if ("image/jpg".equals(mimeTypeFilter)) {
            return openJpgDocument(documentId);
        } else {
            throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter);
        }

    } catch (Exception ex) {
        Log.e(TAG, ex.getMessage());
    } finally {
        return null;
    }
}

@Override
public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {

    // Return all supported MIME tyupes if the client app
    // passes in '*/*' or 'image/*'.
    if ("*/*".equals(mimeTypeFilter) ||
        "image/*".equals(mimeTypeFilter)) {
        return SUPPORTED_MIME_TYPES;
    }

    ArrayList requestedMimeTypes = new ArrayList&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; SUPPORTED_MIME_TYPES.length; i++) {
        if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
            requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
        }
    }
    return (String[])requestedMimeTypes.toArray();
}

セキュリティ

ドキュメント プロバイダがパスワードで保護されたクラウド ストレージ サービスであり、ファイル共有を開始する前にユーザーがログインしていることを確認するとします。ユーザーがログインしていない場合、アプリでどう対処すべきでしょうか。この問題を解決するには、queryRoots() の実装でゼロルートを返します。つまり、下記のように空のルートカーソルを返します。

Kotlin

override fun queryRoots(projection: Array<out String>): Cursor {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

Java

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

もう 1 つのステップでは、getContentResolver().notifyChange() を呼び出します。DocumentsContract について思い返してください。これを使用してこの URI が作成されます。次のスニペットは、ユーザーのログイン ステータスが変わるたびに、ドキュメント プロバイダのルートをクエリするようシステムに指示します。ユーザーがログインしていない場合、queryRoots() を呼び出すと、前述のように空のカーソルが返されます。これにより、ユーザーがプロバイダにログインした場合にのみ、プロバイダのドキュメントを利用できるようになります。

Kotlin

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

Java

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

このページに関連するサンプルコードについては、以下をご覧ください。

このページに関連する動画については、以下をご覧ください。

その他の関連情報については、以下をご覧ください。