ファイルのストレージ サービス(クラウド ストレージ サービスなど)を提供するアプリを開発する場合は、カスタム ドキュメント プロバイダを作成することにより、ストレージ アクセス フレームワーク(SAF)を介してファイルを利用可能にすることができます。このページでは、カスタム ドキュメント プロバイダの作成方法について説明します。
ストレージ アクセス フレームワークの仕組みについては、ストレージ アクセス フレームワークの概要をご覧ください。
マニフェスト
カスタム ドキュメント プロバイダを実装するには、アプリケーションのマニフェストに以下の内容を追加します。
- API レベル 19 以降を対象にする。
<provider>
要素: カスタム ストレージ プロバイダを宣言します。-
DocumentsProvider
サブクラスの名前(以下のパッケージ名を含むクラス名)を設定した属性android:name
: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 以降を実行しているデバイスでのみ使用できます。
Android 4.3 以前を実行しているデバイスに対応するためにアプリケーションで ACTION_GET_CONTENT
をサポートするには、Android 4.4 以降を実行しているデバイスのマニフェストで ACTION_GET_CONTENT
インテントフィルタを無効にする必要があります。ドキュメント プロバイダと ACTION_GET_CONTENT
は相互に排他的であると見なされる必要があります。両方を同時にサポートする場合、システム選択ツール UI にアプリが 2 回表示され、保存されたデータにアクセスする方法が 2 つ提示されます。これはユーザーの混乱の元になります。
Android バージョン 4.4 以降を実行しているデバイスの ACTION_GET_CONTENT
インテント フィルタを無効にするには、次の方法をおすすめします。
res/values/
のbool.xml
リソース ファイルに次の行を追加します。<bool name="atMostJellyBeanMR2">true</bool>
res/values-v19/
のbool.xml
リソース ファイルに次の行を追加します。<bool name="atMostJellyBeanMR2">false</bool>
- アクティビティ エイリアスを追加して、バージョン 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,};
ルートのカーソルには、特定の必須列を含める必要があります。 該当するデータ列は以下のとおりです。
ドキュメントのカーソルには次の必須列を含める必要があります。
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
DocumentsProvider のサブクラスを作成する
カスタム ドキュメント プロバイダ作成の次のステップは、抽象クラス DocumentsProvider
をサブクラス化することです。少なくとも、次のメソッドを実装する必要があります。
実装するうえで厳密に必要となるメソッドは上記のみですが、他にも実装をおすすめするメソッドが数多くあります。詳細については、DocumentsProvider
をご覧ください。
ルートを定義する
queryRoots()
の実装では、DocumentsContract.Root
で定義された列を使用して、ドキュメント プロバイダのすべてのルート ディレクトリを指す Cursor
を返す必要があります。
次のスニペットの projection
パラメータは、呼び出し元が取得したい特定のフィールドを表します。スニペットでは新しいカーソルが作成され、そこに 1 行が追加されます。この行がルート、つまりトップレベル ディレクトリ(Downloads や Images など)になります。ほとんどのプロバイダにはルートが 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 倍を超えるサムネイル画像を返してはいけません。
ドキュメントを開く
openDocument()
を実装して、指定されたファイルを表す ParcelFileDescriptor
を返す必要があります。他のアプリは返された 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
を返すことで、ドキュメント プロバイダのルートに最近更新されたドキュメントの一覧を作成できます。次のコード スニペットでは、 メソッドの実装方法の一例を示します。
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
インテントを送信すると、ユーザーは、ドキュメント プロバイダ内で表示するために、こうした仮想ファイルを選択できます。ユーザーが選択すると、ドキュメント プロバイダが、仮想ファイルを異なるファイル形式ですがオープン可能なファイル形式(画像などの形式)で返します。その結果、クライアント アプリで仮想ファイルが開かれ、ユーザーが表示できるようなります。
プロバイダ内のドキュメントが仮想であることを宣言するには、FLAG_VIRTUAL_DOCUMENT
フラグを queryDocument()
メソッドで返されるファイルに追加します。このフラグは、ファイルに直接のバイトコード表現がなく、直接開くことができないことをクライアント アプリに通知します。
ドキュメント プロバイダ内のファイルが仮想ファイルであることを宣言する場合は、別の MIME タイプ(画像や PDF など)で使用できるようにすることを強くおすすめします。ドキュメント プロバイダで、仮想ファイルでの表示を目的にサポートする代替 MIME タイプを宣言するには、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<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < 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); }
このページに関連するサンプルコードについては、以下をご覧ください。
このページに関連する動画については、以下をご覧ください。
- DevBytes: Android 4.4 ストレージ アクセス フレームワーク: プロバイダ
- ストレージ アクセス フレームワーク: DocumentsProvider のビルド
- ストレージ アクセス フレームワークの仮想ファイル
その他の関連情報については、以下をご覧ください。