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

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

ストレージ アクセス フレームワークの仕組みについて詳しくは、 ストレージ アクセス フレームワークの概要

マニフェスト

カスタム ドキュメント プロバイダを実装するには、アプリケーションの manifest:

  • API レベル 19 以降を対象にする。
  • カスタム ストレージを宣言する <provider> 要素 接続します。
  • 名前に設定されている属性 android:name は、 DocumentsProvider サブクラスを使用すると、 これは、パッケージ名を含むクラス名です。

    com.example.android.storageprovider.MyCloudProvider

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

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

  1. res/values/ の下にある bool.xml リソース ファイルに、次の内容を追加します。 次の行:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ の下にある bool.xml リソース ファイルに、次の内容を追加します。 次の行:
    <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>
    

契約

通常、カスタム コンテンツ プロバイダを記述する際のタスクの 1 つは、 コントラクト クラスの実装方法については、 <ph type="x-smartling-placeholder"></ph> コンテンツ プロバイダのデベロッパー ガイドをご覧ください。コントラクト クラスは public final クラス URI、列名、MIME タイプ、 メタデータが含まれます。SAF コントラクト クラスが用意されているため、API を記述する必要はありません。 所有する:

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

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() の実装では、すべてを指す Cursor を返す必要があります。 ドキュメント プロバイダのルート ディレクトリから参照できます。 DocumentsContract.Root

次のスニペットでは、projection パラメータは 指定することもできます。スニペットによって、新しいカーソルが そこに 1 行が追加されます。つまり、1 つのルート、最上位のディレクトリ、 ダウンロードまたは画像ですほとんどのプロバイダにはルートが 1 つしかありませんが、複数ある場合もありますが、 複数のユーザーアカウントがある場合などですそのような場合は カーソル位置まで移動します。

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 など)に接続する場合 ユーザーがログアウトできるアカウントがあれば、 を使用してドキュメントの UI を更新し、変更の同期を維持できます。 ContentResolver.notifyChange() メソッドを使用します。これは次のコード スニペットのようになります。

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() Cursor を返す必要があります。 ディレクトリで定義した列を使用して、指定したディレクトリに DocumentsContract.Document

このメソッドは、ユーザーが選択ツール UI でルートを選択すると呼び出されます。 このメソッドは、指定されたドキュメント ID の子を取得します。 COLUMN_DOCUMENT_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() 指定されたファイルを指す Cursor を返す必要があります。 DocumentsContract.Document で定義された列を使用します。

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

注意: ドキュメント プロバイダは 2 倍を超えるサムネイル画像を返すことはできません sizeHint パラメータで指定されたサイズ。

ドキュメントを開く

以下を表す 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);
    }
}

ドキュメント プロバイダがファイルをストリーミングする場合や、 設計したい場合は、Terraform モジュールの実装を createReliablePipe() または createReliableSocketPair() メソッド。 これらの方法では、IP アドレスのペアを ParcelFileDescriptor オブジェクト(ここでは 1 つを返すことができます) もう一方は 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()。Terraform Registry の実装には、 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 列 サポートされる機能を示しますまた、kubectl の 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)で導入された機能で、ドキュメント プロバイダ 拡張子が付いていないファイルには、 直接バイトコード表現です。他のアプリが仮想ファイルを表示できるようにするには、 ドキュメント プロバイダが別のオープン可能なファイルを生成する必要がある 必要があります。

たとえば、ドキュメント プロバイダに 他のアプリでは直接開けない形式のファイルで、実質的には仮想ファイルです。 クライアント アプリが ACTION_VIEW インテントを送信したとき CATEGORY_OPENABLE カテゴリを指定しないと、 ユーザーはドキュメント プロバイダ内のこれらの仮想ファイルを 表示されます。ドキュメント プロバイダが仮想ファイルを 画像などの別のオープン可能なファイル形式で レンダリングされます その後、クライアント アプリで仮想ファイルを開いてユーザーが閲覧できます。

プロバイダのドキュメントが仮想ドキュメントであることを宣言するには、 FLAG_VIRTUAL_DOCUMENT コマンドによって返されたファイルに queryDocument() メソッドを呼び出します。このフラグは、ファイルに直接リンクがないことをクライアント アプリに警告します。 直接開くことはできません。

ドキュメント プロバイダ内のファイルが仮想ファイルであると宣言すると、 別の環境でも利用できるようにすることを 画像や PDF などの MIME タイプです。ドキュメント プロバイダ 代替 MIME タイプを宣言し、 仮想ファイルの表示をサポートするには、Terraform の getDocumentStreamTypes() メソッドを呼び出します。クライアント アプリが 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);
}

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

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

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