Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

ストレージ アクセス フレームワークを使用してファイルを開く

Android 4.4(API レベル 19)は、ストレージ アクセス フレームワーク(SAF)を採用しています。SAF を利用することで、ユーザーは設定したドキュメント ストレージ プロバイダ全体から簡単にドキュメント、画像、その他のファイルを参照して開くことができます。標準の使いやすい UI により、アプリやプロバイダを通じて一貫性のある方法でファイルを参照したり、最近使用したファイルにアクセスしたりできます。

サービスをカプセル化する DocumentsProvider を実装することで、クラウドやローカル ストレージ サービスをエコシステムに参加させることができます。プロバイダのドキュメントへのアクセスが必要なクライアント アプリも、数行のコードだけで SAF と統合できます。

SAF には次の項目が含まれます。

  • ドキュメント プロバイダ - ストレージ サービス(Google ドライブなど)が管理するファイルの表示を許可するコンテンツ プロバイダです。ドキュメント プロバイダは DocumentsProvider クラスのサブクラスとして実装されます。document-provider スキーマは従来のファイル階層に基づくものですが、ドキュメント プロバイダが物理的にどのようにデータを格納するかはその設定次第です。Android プラットフォームには、ダウンロード、画像、動画などの組み込みのドキュメント プロバイダがいくつか用意されています。
  • クライアント アプリ - ACTION_OPEN_DOCUMENT または ACTION_CREATE_DOCUMENT インテントを呼び出し、ドキュメント プロバイダが返したファイルを受け取るカスタムアプリです。
  • ピッカー - クライアント アプリの検索条件を満たすすべてのドキュメント プロバイダのドキュメントにアクセスできるシステム UI です。

SAF は次のような機能も提供します。

  • ユーザーは 1 つのアプリだけではなく、すべてのドキュメント プロバイダのコンテンツを参照できます。
  • アプリからドキュメント プロバイダが所有するドキュメントへの、長期間の固定アクセスを可能にします。このアクセスにより、ユーザーはプロバイダ上でのファイルの追加、編集、保存、削除が可能になります。
  • 複数のユーザー アカウントと USB ストレージ プロバイダなどの一時的なルートをサポートします。一時的なルートはドライブを挿入した場合にのみ表示されます。

概要

SAF は、DocumentsProvider クラスのサブクラスであるコンテンツ プロバイダを中心に展開します。ドキュメント プロバイダでは、データは以下の図のように従来のファイル階層で構造化されます。

データモデル

図 1. ドキュメント プロバイダのデータモデル。1 つのルートが 1 つのドキュメントを指し、そこからツリー全体が広がります。

以下の点に注意してください。

  • 各ドキュメント プロバイダは、ドキュメントのツリーの検索の出発点である 1 つ以上の「ルート」を報告します。各ルートは一意の COLUMN_ROOT_ID を持ち、該当するルートの下のコンテンツを表すドキュメント(ディレクトリ)を指します。ルートはデザインによって動的に変化し、複数アカウント、一時的な USB ストレージ ドライブ、ユーザーのログインとログアウトなどのユースケースをサポートします。
  • 各ルートの下のドキュメントは 1 つだけになります。そのドキュメントは、1~N 個のドキュメントを指し、さらにそれぞれのドキュメントも 1~N 個のドキュメントを指すことができます。
  • 各ストレージ バックエンドは、一意の COLUMN_DOCUMENT_ID を使って個々のファイルやディレクトリを参照して表示します。端末の再起動後も使用できる永続的な URI の付与に使用することから、ドキュメント ID は一意でなければならず、一度発行すると変更できません。
  • ドキュメントには、開くことができるファイル(特定の MIME タイプを持つもの)か、追加のドキュメント(MIME_TYPE_DIR MIME タイプを持つもの)を含むディレクトリのいずれかに設定できます。
  • 各ドキュメントはさまざまな機能を持つことができ、COLUMN_FLAGS を使って記述します。たとえば、FLAG_SUPPORTS_WRITEFLAG_SUPPORTS_DELETEFLAG_SUPPORTS_THUMBNAIL といった機能です。同一の COLUMN_DOCUMENT_ID は複数のディレクトリに含めることができます。

制御フロー

前述のように、ドキュメント プロバイダのデータモデルは従来のファイル階層を基本とします。ただし、DocumentsProvider API でアクセスできる場合は、任意の方法でデータを物理的に格納できます。たとえば、データにタグベースのクラウド ストレージを使用できます。

図 2 は、写真アプリが格納されたデータに SAF を使ってアクセスする様子を表しています。

アプリ

図 2. ストレージ アクセス フレームワークのフロー

以下の点に注意してください。

  • SAF では、プロバイダとクライアントは直接やり取りできません。クライアントが、ファイルを操作(ファイルの読み取り、編集、作成、削除)するためのパーミッションを要求します。
  • アプリ(この例では写真アプリ)がインテント ACTION_OPEN_DOCUMENT または ACTION_CREATE_DOCUMENT を起動すると、やり取りが始まります。インテントには、基準を詳細に絞り込むためのフィルタが含まれる場合があります。たとえば、「画像」の MIME タイプを持つ、開くことができるすべてのファイルを取得する、のように設定できます。
  • インテントが起動すると、システム ピッカーが登録済みの各プロバイダに移動し、一致するコンテンツのルートをユーザーに表示します。
  • 基礎となるドキュメント プロバイダの種類に関係なく、ドキュメントにアクセスするための標準的なインターフェースがピッカーから提供されます。図 2 には、Google ドライブ プロバイダ、USB プロバイダ、クラウド プロバイダの例を示しています。

図 3 は、Google ドライブ アカウントを選択したユーザーが画像を検索するピッカーを表しています。クライアント アプリで利用可能なすべてのルートも表示されます。

ピッカー

図 3. ピッカー

ユーザーが Google ドライブを選択すると、図 4 のように画像が表示されます。この時点から、ユーザーはプロバイダとクライアント アプリがサポートする方法で、画像を操作できるようになります。

ピッカー

図 4. 画像

クライアント アプリを作成する

Android 4.3 以前では、別のアプリからファイルを取得する場合、ACTION_PICKACTION_GET_CONTENT といったインテントを呼び出す必要があります。その後ユーザーは、ファイルを選択するためのアプリを 1 つ選びます。選択されたアプリはユーザーが使用可能なファイルを参照して選択するためのユーザー インターフェースを提供する必要があります。

Android 4.4 以降であれば、ACTION_OPEN_DOCUMENT インテントを使用するという選択肢もあります。このインテントではシステム制御のピッカー UI が表示され、ユーザーはそこから他のアプリで利用可能なすべてのファイル参照できます。ユーザーは、この 1 つの UI から、サポートされるすべてのアプリのファイルを選択できます。

ACTION_OPEN_DOCUMENT は、ACTION_GET_CONTENT を置き換えることを意図したものではありません。どちらを使用するかは、アプリのニーズによって異なります。

  • アプリでデータの読み取りとインポートのみを行う場合は、ACTION_GET_CONTENT を使用します。この方法を使用すると、アプリは、画像ファイルなどのデータのコピーをインポートします。
  • アプリからドキュメント プロバイダが所有するドキュメントへの、長期間の固定アクセスを可能にするには、ACTION_OPEN_DOCUMENT を使用します。ドキュメント プロバイダに格納されている画像をユーザーが編集するための、写真編集アプリなどの例が挙げられます。

ここでは、ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT インテントに基づいたクライアント アプリの作成方法について説明します。

次のスニペットでは ACTION_OPEN_DOCUMENT を使って、画像ファイルがあるドキュメント プロバイダを検索します。

Kotlin

private const val READ_REQUEST_CODE: Int = 42
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
fun performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        // Filter to only show results that can be "opened", such as a
        // file (as opposed to a list of contacts or timezones)
        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 "*/*".
        type = "image/*"
    }

    startActivityForResult(intent, READ_REQUEST_CODE)
}

Java

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

以下の点に注意してください。

  • アプリが ACTION_OPEN_DOCUMENT インテントを開始すると、ピッカーが起動し、条件に一致するすべてのドキュメント プロバイダが表示されます。
  • カテゴリ CATEGORY_OPENABLE をインテントに追加すると、結果がフィルタリングされ、画像ファイルなどの開くことができるドキュメントのみが表示されます。
  • intent.setType("image/*") 文で結果をさらにフィルタリングし、画像 MIME データタイプのドキュメントのみを表示します。

プロセスの結果

ユーザーがピッカーでドキュメントを選択すると、onActivityResult() が呼び出されます。選択したドキュメントを指す URI は resultData パラメータに含まれます。getData() を使って URI を抽出します。URI を抽出したら、その URI を使ってユーザーが希望するドキュメントを取得できます。次に例を示します。

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {

    // 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().
        resultData?.data?.also { uri ->
            Log.i(TAG, "Uri: $uri")
            showImage(uri)
        }
    }
}

Java

@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 で指定したドキュメントのメタデータを取得して、ログに記録します。

Kotlin

fun 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.
    val cursor: Cursor? = contentResolver.query( uri, null, null, null, null, null)

    cursor?.use {
        // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (it.moveToFirst()) {

            // Note it's called "Display Name".  This is
            // provider-specific, and might not necessarily be the file name.
            val displayName: String =
                    it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
            Log.i(TAG, "Display Name: $displayName")

            val sizeIndex: Int = it.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.
            val size: String = if (!it.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                it.getString(sizeIndex)
            } else {
                "Unknown"
            }
            Log.i(TAG, "Size: $size")
        }
    }
}

Java

public void dumpImageMetaData(Uri uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since we want
    // all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
    // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name".  This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null.  But since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "unpredictable".  So as
            // a rule, check if it's null before assigning to an int.  This will
            // happen often:  The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

ドキュメントを開く

ドキュメントの URI を取得したら、そのドキュメントを開いてさまざまな操作を行うことができます。

ビットマップ

Bitmap を開く方法の例を次に示します。

Kotlin

@Throws(IOException::class)
private fun getBitmapFromUri(uri: Uri): Bitmap {
    val parcelFileDescriptor: ParcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")
    val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
    val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
    parcelFileDescriptor.close()
    return image
}

Java

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

この操作を UI スレッドで実行しないでください。AsyncTask を使ってバックグラウンドで実行します。ビットマップを開いたら、ImageView で表示できます。

InputStream を取得する

URI から InputStream を取得する方法の例を次に示します。このスニペットでは、ファイルの行が文字列に読み込まれます。

Kotlin

@Throws(IOException::class)
private fun readTextFromUri(uri: Uri): String {
    val stringBuilder = StringBuilder()
    contentResolver.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                stringBuilder.append(line)
                line = reader.readLine()
            }
        }
    }
    return stringBuilder.toString()
}

Java

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

ドキュメントを作成する

ACTION_CREATE_DOCUMENT インテントを使用すると、アプリでドキュメント プロバイダに新しいドキュメントを作成できます。ファイルを作成するには、インテントに MIME タイプとファイル名を渡し、一意の要求コードを使ってインテントを起動します。後は自動で行われます。

Kotlin

// 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 const val WRITE_REQUEST_CODE: Int = 43
...
private fun createFile(mimeType: String, fileName: String) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        // Filter to only show results that can be "opened", such as
        // a file (as opposed to a list of contacts or timezones).
        addCategory(Intent.CATEGORY_OPENABLE)

        // Create a file with the requested MIME type.
        type = mimeType
        putExtra(Intent.EXTRA_TITLE, fileName)
    }

    startActivityForResult(intent, WRITE_REQUEST_CODE)
}

Java

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

新しいドキュメントを作成したら、onActivityResult() で URI を取得して、ドキュメントに書き込み操作を実行できます。

ドキュメントを削除する

ドキュメントの URI と、SUPPORTS_DELETE を含むドキュメントの Document.COLUMN_FLAGS を取得すると、ドキュメントを削除できます。次に例を示します。

Kotlin

DocumentsContract.deleteDocument(contentResolver, uri)

Java

DocumentsContract.deleteDocument(getContentResolver(), uri);

ドキュメントを編集する

SAF を使用するとテキスト ドキュメントを直接編集できます。このスニペットは ACTION_OPEN_DOCUMENT インテントを起動し、カテゴリ CATEGORY_OPENABLE を使って、開くことができるドキュメントのみを表示します。次のように、テキスト ファイルのみを表示するようにフィルタリングします。

Kotlin

private const val EDIT_REQUEST_CODE: Int = 44
/**
 * Open a file for writing and append some text to it.
 */
private fun editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        // Filter to only show results that can be "opened", such as a
        // file (as opposed to a list of contacts or timezones).
        addCategory(Intent.CATEGORY_OPENABLE)

        // Filter to show only text files.
        type = "text/plain"
    }

    startActivityForResult(intent, EDIT_REQUEST_CODE)
}

Java

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

次に、onActivityResult()プロセスの結果をご覧ください)からコードを呼び出して、編集を行います。次のスニペットは ContentResolver から FileOutputStream を取得します。デフォルトでは、書き込みモードを使用します。必要最小限のアクセスのみを要求することをお勧めします。書き込みのみが必要な場合に読み込みと書き込みを要求しないでください。

Kotlin

private fun alterDocument(uri: Uri) {
    try {
        contentResolver.openFileDescriptor(uri, "w")?.use {
            // use{} lets the document provider know you're done by automatically closing the stream
            FileOutputStream(it.fileDescriptor).use {
                it.write(
                    ("Overwritten by MyCloud at ${System.currentTimeMillis()}\n").toByteArray()
                )
            }
        }
    } catch (e: FileNotFoundException) {
        e.printStackTrace()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Java

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 パーミッションが付与されます。このパーミッションはユーザーの端末が再起動されるまで有効です。しかし、画像編集アプリでユーザーが最近編集した 5 つの画像にアプリから直接アクセスできるようにするにはどうすればいいでしょう。ユーザーの端末が再起動されたら、ファイルを検索するためにユーザーをシステム ピッカーに戻す必要がありますが、これは適切な方法とは言えません。

このような状況を避けるために、システムがアプリに付与するパーミッションを固定します。実際は、システムが提供する固定可能な URI パーミッションをアプリで「取得」します。これにより、端末を再起動した場合でも、ユーザーはアプリからファイルに継続的にアクセスできます。

Kotlin

val takeFlags: Int = intent.flags and
        (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

Java

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

最後にもう 1 つ手順があります。アプリが最近アクセスした URI は、既に無効になっている場合があります。別のアプリによってドキュメントが削除されたり、修正されたりすることが考えられます。そこで、最新のデータを確認するには、常に getContentResolver().takePersistableUriPermission() を呼び出す必要があります。

仮想ファイルを開く

Android 7.0 では、ストレージ アクセス フレームワークに仮想ファイルの概念が追加されています。仮想ファイルにはバイナリ表現はありませんが、クライアント アプリはこれらを別のファイル形式に変換するか、ACTION_VIEW インテントを使用してそれらのファイルを表示することで、コンテンツを開くことができます。

仮想ファイルを開くには、クライアント アプリにそのファイルを処理するための特別なロジックを含める必要があります。たとえば、ファイルをプレビューするために、ファイルをバイトで表現する場合は、ドキュメント プロバイダから代替 MIME タイプを要求する必要があります。

アプリで仮想ドキュメントの URI を取得するには、ドキュメントを検索するで示したコードのように、Intent を作成してからファイル ピッカーの UI を開きます。

ユーザーが選択すると、プロセスの結果で示したように、システムは onActivityResult() メソッドを呼び出します。アプリはファイルの URI を取得してから、次のコード スニペットと同様なメソッドを使用して、ファイルが仮想ファイルかどうかを判断できます。

Kotlin

private fun isVirtualFile(uri: Uri): Boolean {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false
    }

    val cursor: Cursor? = contentResolver.query(
            uri,
            arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
            null,
            null,
            null
    )

    val flags: Int = cursor?.use {
        if (cursor.moveToFirst()) {
            cursor.getInt(0)
        } else {
            0
        }
    } ?: 0

    return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
}

Java

private boolean isVirtualFile(Uri uri) {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false;
    }

    Cursor cursor = getContentResolver().query(
        uri,
        new String[] { DocumentsContract.Document.COLUMN_FLAGS },
        null, null, null);

    int flags = 0;
    if (cursor.moveToFirst()) {
        flags = cursor.getInt(0);
    }
    cursor.close();

    return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}

ファイルが仮想ファイルであることを確認したら、そのファイルを画像ファイルなどの代替 MIME タイプに変換できます。次のコード スニペットは、仮想ファイルを画像として表現できるかどうかを確認する方法を示し、表現できる場合は仮想ファイルから入力ストリームを取得します。

Kotlin

@Throws(IOException::class)
private fun getInputStreamForVirtualFile(uri: Uri, mimeTypeFilter: String): InputStream {

    val openableMimeTypes: Array<String>? = contentResolver.getStreamTypes(uri, mimeTypeFilter)

    return if (openableMimeTypes?.isNotEmpty() == true) {
        contentResolver
                .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
                .createInputStream()
    } else {
        throw FileNotFoundException()
    }
}

Java


private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
    throws IOException {

    ContentResolver resolver = getContentResolver();

    String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);

    if (openableMimeTypes == null ||
        openableMimeTypes.length &lt; 1) {
        throw new FileNotFoundException();
    }

    return resolver
        .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
        .createInputStream();
}

仮想ファイルの詳細と、ストレージ アクセス フレームワーク クライアント アプリで仮想ファイルを処理する方法については、ストレージ アクセス フレームワークにおける仮想ファイルの動画をご覧ください。

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

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

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