Android Q のプライバシーに関する変更点: 対象範囲別ストレージ

Android 9(API レベル 28)以前をターゲットとしているアプリの場合、Android Q ベータ版 5 でも、ストレージのデフォルト動作は以前の Android バージョンと変わりません。対象範囲別ストレージを使用できるように既存のアプリを更新する場合は、Android 9 以前をターゲットにしているアプリでも、新しい requestLegacyExternalStorage マニフェスト属性を使用して、Android Q デバイス上で新しい動作を有効にすることができます。

Android Q では、ユーザーがファイルを詳細に管理して整理できるように、デバイスの外部ストレージ上にあるファイル(パス「/sdcard」に保存されているファイルなど)にアプリがアクセスする方法が変更されています。Android Q は、引き続き READ_EXTERNAL_STORAGE 権限および WRITE_EXTERNAL_STORAGE 権限を使用します(これはユーザー向け STORAGE ランタイム権限に対応します)。ただし、Android Q をターゲットとしているアプリ(ならびにこの変更を有効にしたアプリ)の場合は、外部ストレージ用の限定ビューがデフォルトで用意されます。そのようなアプリの場合、アプリ固有ディレクトリと特定のタイプのメディアしか表示されないため、追加のユーザー権限をリクエストする必要はありません。

このガイドでは、限定ビューに表示されるファイルについて整理し、外部ストレージ デバイス上に保存したファイルに対するアクセス、共有、編集を引き続き行うことができるようにアプリを更新する方法について説明します。また、写真内の位置情報や、ネイティブ コードからのメディア アクセス、コンテンツ クエリ内での列名の使用に関する注意事項についても説明します。

Android Q で導入された外部ストレージに関する変更の詳細については、外部ストレージ上にファイルを作成する際の改善点の説明をご覧ください。

外部ストレージ用の限定ビュー

Android Q をターゲットにしているアプリの場合、外部ストレージ デバイス上のファイルを表示するための「限定ビュー」がデフォルトで用意されています。各アプリは、Context.getExternalFilesDir() を使用することで、アプリ固有ディレクトリの下にそのアプリ用のファイルを保存できます。

限定ビューを持つアプリは、アプリ固有ディレクトリの内部と外部の両方で、作成したファイルに対する読み書きアクセス権限を常に有します。アプリがこのようなファイルにアクセスするために STORAGE パーミッションを宣言する必要はありません。

他のアプリが作成したファイルにアクセスできるのは、以下の両方の条件が満たされている場合に限られます。

  1. アプリに READ_EXTERNAL_STORAGE パーミッションが付与されていること。
  2. 対象ファイルが、明確に定義された次のいずれかのメディア コレクション内にあること。

別のアプリが作成した他のファイル(「ダウンロード」ディレクトリ内のファイルを含む)にアクセスできるようにするには、アプリがストレージ アクセス フレームワークを使用する必要があります。ストレージ アクセス フレームワークにより、ユーザーは特定のファイルを選択できます。

また、限定ビューの場合、メディア関連データに関して以下の制限が適用されます。

  • アプリに ACCESS_MEDIA_LOCATION 権限が付与されていない場合は、画像ファイル内の Exif メタデータが変更されます。詳細については、写真内の位置情報にアクセスする方法をご覧ください。
  • メディアストア内にある各ファイルの DATA 列が削除されます。
  • MediaStore.Files テーブル自体も限定され、写真、動画、音声ファイルだけが表示されます。たとえば、PDF ファイルはこのテーブルには表示されません。

ネイティブ コードでメディア ファイルにアクセスするには、Java ベースまたは Kotlin ベースのコード内で MediaStore を使用してファイルを取得し、対応するファイル記述子をネイティブ コードに渡します。詳細については、ネイティブ コードからメディア ファイルにアクセスする方法をご覧ください。

アンインストール後もアプリのファイルを残す

外部ストレージ用の限定ビューを持つアプリをアンインストールすると、アプリ固有ディレクトリ内にあるファイルはすべてクリーンアップされます。アンインストール後にファイルを残すには、MediaStore 内のディレクトリに保存してください。

限定ビューを無効にする

ストレージに関するおすすめの方法に沿って開発されたアプリであれば、最小限の変更を加えるだけで、対象範囲別ストレージと連携できるはずです。アプリの完全互換性を確保する前やアプリのテストを行う前に、アプリのターゲット SDK レベルまたは新しいマニフェスト属性 requestLegacyExternalStorage に基づいて、対象範囲別ストレージ機能を一時的に無効にすることができます。

  • Android 9(API レベル 28)以前をターゲットにします。

  • Android Q をターゲットにしている場合は、アプリのマニフェスト ファイル内で requestLegacyExternalStorage の値を true に設定します。

        <manifest ... >
          <!-- This attribute is "false" by default on apps targeting Android Q. -->
          <application android:requestLegacyExternalStorage="true" ... >
            ...
          </application>
        </manifest>
        

アプリが以前の外部ストレージを有効にしてインストールされている場合、アプリはアンインストールされるまでこのモードのままです。この互換動作は、Android Q を稼働するようにデバイスが後でアップグレードされるかどうか、および Android Q をターゲットにするようにアプリが後で更新されるかどうかとは無関係です。

仮想外部ストレージ デバイスをセットアップする

リムーバブル外部ストレージのないデバイスでは、次のコマンドを使用することで、仮想ディスクを有効にしてテストすることができます。

    adb shell sm set-virtual-disk true
    

限定ビューによるファイル アクセスの概要

外部ストレージ用の限定ビューを持つアプリがファイルにアクセスする方法を、以下の表にまとめます。

ファイルの場所 必要なパーミッション アクセス方法(*) アプリをアンインストールするとファイルも削除されるか?
アプリ固有ディレクトリ なし getExternalFilesDir()
メディア コレクション
(写真、動画、音声)
READ_EXTERNAL_STORAGE

他のアプリのファイルにアクセスする場合のみ
MediaStore ×
ダウンロード
(ドキュメント、
電子書籍)
なし ストレージ アクセス フレームワーク
(システムのファイル選択ツールを読み込みます)
×

* ストレージ アクセス フレームワークを使用すると、権限をリクエストすることなく、上記の表に示されている各場所にアクセスすることができます。

変更に合わせて特定の使用パターンを調整する

このセクションでは、Android Q をターゲットとするアプリで生じるストレージ機能の変更に合わせて、特定のタイプのメディア関連アプリを最適化する際のアドバイスを紹介します。

アプリ固有ディレクトリ内や MediaStore 内に存在していないファイルにアクセスする必要がない限り、限定ビューを使用することをおすすめします。

メディア ファイルを共有する

一部のアプリでは、ユーザーはメディア ファイルを互いに共有できます。たとえば、ソーシャル メディア アプリを使用すると、写真や動画を友だちと共有できます。

ユーザーが共有するメディア ファイルにアクセスするには、MediaStore API を使用します。この API を使用して、アプリ経由で受信したファイルを保存することができます。Android Q で導入された改善点の説明をご覧ください。

メッセージ アプリやプロファイル アプリなど、コンパニオン アプリを提供する場合は、content:// URI を使用してファイルの共有を設定します。このワークフローは、セキュリティに関するおすすめの方法としても推奨されています。

ドキュメントを使用する

一部のアプリは、ユーザーが同僚と共有したり他のドキュメントに読み込んだりするデータを入力するストレージ ユニットとしてドキュメントを使用します。たとえば、ユーザーがビジネス生産性向上マニュアルを開く場合や、EPUB ファイルとして保存された書籍を開く場合などが該当します。

このような場合、システムのファイル選択アプリを開く ACTION_OPEN_DOCUMENT インテントを呼び出して、ユーザーが対象ファイルを選択できるようにします。アプリがサポートしているファイル形式だけを表示するには、Intent.EXTRA_MIME_TYPES エクストラをインテント内に組み込みます。

ユーザーの同意を得た後に ACTION_OPEN_DOCUMENT を使用してファイルを開く方法については、GitHub の ActionOpenDocument サンプルをご覧ください。

ファイルのグループを管理する

ファイル管理アプリやメディア作成アプリは通常、ディレクトリ階層内のファイルのグループを管理します。このようなアプリの場合、ACTION_OPEN_DOCUMENT_TREE インテントを呼び出して、ディレクトリ ツリー全体へのアクセス権限をユーザーに許可してもらいます。このようなアプリでは、選択されたディレクトリとそのサブディレクトリ内の任意のファイルを編集することができます。

このインターフェースを使用することで、ユーザーは、ローカルベースやクラウドベースのソリューションがサポートできる DocumentsProvider のインストール済みインスタンスからファイルにアクセスすることができます。

ユーザーの同意を得た後に ACTION_OPEN_DOCUMENT_TREE を使用してディレクトリ ツリーを開く方法については、GitHub の ActionOpenDocumentTree サンプルをご覧ください。

メディア コンテンツにアクセスして編集する

このセクションでは、Android Q 上でアプリを稼働しても引き続き優れたユーザー エクスペリエンスを提供できるように、メディア ファイルを読み込んで外部ストレージに保存する際のおすすめの方法を紹介します。

注: 外部ストレージ用の限定ビューを持つアプリが STORAGE ランタイム権限をリクエストした場合、そのアプリで表示できるのは、アプリ固有ディレクトリ内か以下のいずれかのメディア コレクション内にあるファイルに限られます。

STORAGE パーミッションが付与されたとしても、外部ストレージ デバイスの未加工ファイル システム ビューにアクセスするアプリの場合、アクセスできるのは、アプリの未加工パッケージ固有パスに限られます。アプリが未加工ファイル システム ビューを使用してパッケージ固有パスの外部にあるファイルを開こうとすると、エラーが発生します。

  • マネージコードの場合、FileNotFoundException が発生します。
  • ネイティブ コードの場合、EPERM カーネルエラーが発生します。

ファイルにアクセスする

サポート終了済みの DATA 列を使用してメディア ファイルを読み込まないでください。代わりに、ContentResolver から以下のいずれかのメソッドを呼び出します。

  • 単一のメディア ファイルのサムネイルの場合、loadThumbnail() を使用して、読み込むサムネイルのサイズを渡します。
  • 単一のメディア ファイルの場合、openFileDescriptor() を使用します。
  • メディア ファイルのコレクションの場合、query() を使用します。

メディア ファイルにアクセスする方法を次のコード スニペットに示します。

    // Load thumbnail of a specific media item.
    val mediaThumbnail = resolver.loadThumbnail(item, Size(640, 480), null)

    // Open a specific media item.
    resolver.openFileDescriptor(item, mode).use { pfd ->
        // ...
    }

    // Find all videos on a given storage device, including pending files.
    val collection = MediaStore.Video.Media.getContentUri(volumeName)
    val collectionWithPending = MediaStore.setIncludePending(collection)
    resolver.query(collectionWithPending, null, null, null).use { c ->
        // ...
    }
    

ネイティブ コードからアクセスする

別のアプリと共有しているファイルや、ユーザーのメディア コレクション内のメディア ファイルなど、アプリのネイティブ コード内で特定のメディア ファイルを処理することが必要になる場合があります。このような場合、まず Java ベースまたは Kotlin ベースのコード内でそのメディア ファイルを探してから、そのファイルに関連付けられたファイル記述子をネイティブ コードに渡します。

メディア オブジェクトのファイル記述子をアプリのネイティブ コードに渡す方法を次のコード スニペットに示します。

Kotlin

    val contentUri: Uri =
            ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(BaseColumns._ID))
    val fileOpenMode = "r"
    val parcelFd = resolver.openFileDescriptor(uri, fileOpenMode)
    val fd = parcelFd?.detachFd()
    // Pass the integer value "fd" into your native code. Remember to call
    // close(2) on the file descriptor when you're done using it.
    

Java

    Uri contentUri = ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(Integer.parseInt(BaseColumns._ID)));
    String fileOpenMode = "r";
    ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);
    if (parcelFd != null) {
        int fd = parcelFd.detachFd();
        // Pass the integer value "fd" into your native code. Remember to call
        // close(2) on the file descriptor when you're done using it.
    }
    

ネイティブ コード内でファイルにアクセスする方法については、Android Dev Summit 2018 の Files for Miles の講演(15 分 20 秒から)をご覧ください。

他のアプリのメディア ファイルを更新する

別のアプリが元々外部ストレージ デバイスに保存したメディア ファイルを編集するには、プラットフォームがスローする RecoverableSecurityException をキャッチします。そして、以下のコード スニペットに示すように、アプリにそのアイテムへの書き込みアクセス権限を付与するようユーザーにリクエストします。

Kotlin

    try {
        // ...
    } catch (rse: RecoverableSecurityException) {
        val requestAccessIntentSender = rse.userAction.actionIntent.intentSender

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null)
    }
    

Java

    try {
        // ...
    } catch (RecoverableSecurityException rse) {
        IntentSender requestAccessIntentSender = rse.getUserAction()
                .getActionIntent().getIntentSender();

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null);
    }
    

写真内の位置情報

写真によっては、Exif メタデータ内に位置情報が格納されており、写真の撮影場所をユーザーが確認できる場合があります。この位置情報は機密情報であるため、Android Q では、外部ストレージ用の限定ビューを持つアプリの場合、この情報はデフォルトで非表示になっています。位置情報に関するこの制限は、カメラ特性に適用される制限とは異なります。

写真の位置情報にアクセスする必要のあるアプリの場合、次の手順を行ってください。

  1. アプリのマニフェストに新しい ACCESS_MEDIA_LOCATION 権限を追加します
  2. MediaStore オブジェクトから setRequireOriginal() を呼び出して、写真の URI を渡します。

このプロセスの例を次のコード スニペットに示します。

Kotlin

    // Get location data from the ExifInterface class.
    val photoUri = MediaStore.setRequireOriginal(photoUri)
    contentResolver.openInputStream(photoUri).use { stream ->
        ExifInterface(stream).run {
            // If lat/long is null, fall back to the coordinates (0, 0).
            val latLong = ?: doubleArrayOf(0.0, 0.0)
        }
    }
    

Java

    Uri photoUri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex));

    final double[] latLong;

    // Get location data from the ExifInterface class.
    photoUri = MediaStore.setRequireOriginal(photoUri);
    InputStream stream = getContentResolver().openInputStream(photoUri);
    if (stream != null) {
        ExifInterface exifInterface = new ExifInterface(stream);
        double[] returnedLatLong = exifInterface.getLatLong();

        // If lat/long is null, fall back to the coordinates (0, 0).
        latLong = returnedLatLong != null ? returnedLatLong : new double[2];

        // Don't reuse the stream associated with the instance of "ExifInterface".
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
    

コンテンツ クエリの列名

mime_type AS MimeType など、列名の代替表記法をアプリのコード内で使用している場合、Android Q では、MediaStore API 内で定義されている列名が必要になります。

MimeType など、Android API 内で定義されていない列名を想定したライブラリにコードが依存している場合、CursorWrapper を使用して、アプリのプロセス内で列名を動的に変換してください。