アンドロイド - MediaStoreにメディアファイルを保存する方法

Android10(Q)でScoped Storageが適用され、メディアデータを保存するときは、MediaStoreを利用することをお勧めしています。 Scoped Storageを使用して、メディアデータを保存することができますが、MediaStoreはデータを保存するために、別の権限を必要としないからです。

Android10(Q)以前にもMediaStore(Media Provider)を介してdataをinsertすることができました。 PとQのMediaStoreにデータを保存する方法を調べて、違いに話してみましょう。

MediaStoreでデータをqueryする方法は、前回の記事で紹介しました。

MediaStoreにInsertする方法(Android 10、Q)

Android 10(Q)でMediaStoreを介してデータを読み取るときは、 READ_EXTERNAL_STORAGE権限を要求し、書くとき何の権限を必要としません。

次のコードは、MediaStoreにImageを保存する例を示します。

val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image_q.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

val collection = MediaStore.Images.Media
    .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = contentResolver.insert(collection, values)!!

contentResolver.openFileDescriptor(item, "w", null).use {
    // write something to OutputStream
    FileOutputStream(it!!.fileDescriptor).use { outputStream ->
        val imageInputStream = resources.openRawResource(R.raw.my_image)
        while (true) {
            val data = imageInputStream.read()
            if (data == -1) {
                break
            }
            outputStream.write(data)
        }
        imageInputStream.close()
        outputStream.close()
    }
}

values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(item, values, null, null)

コードは次の順序で行われます。

  • ContentResolver에ContentValues를insert
  • ContentResolverのFDにファイルをwrite
  • write完了後contentResolver update

上記のコードを小さく分けて説明します。

ContentValues

ContentValuesクラスはContentResolverにデータを保存するために使用します。

ContentValuesにファイル情報を入力して、 insert()の引数として保存URIとContentValuesを転送します。 次に、登録されたファイルを指しているUriオブジェクトが返されます。

val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image_q.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

val collection = MediaStore.Images.Media
    .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item: Uri = contentResolver.insert(collection, values)!!

リターンされたUriにデータwriteを行うことができます。

上記のコードでは IS_PENDINGという属性を1にsetする部分がありますが。この属性は、まだ私は、ファイルをwriteしていないので、他の場所では、データを要求すると無視するようにという意味です。 すべてのファイルをwriteした後、このプロパティを0にupdateしてくれるとします。

put(MediaStore.Images.Media.IS_PENDING, 1)

Data保存

Insertで得たUriにファイルをwriteします。 Qで権限なしMediaStoreにアクセスするため、ファイルの絶対パスを知ることができません。 Uriを介してFDを得、FDに私は登録したいファイルをwriteしてくれるとします。

UriでFDを得writeするコードは次のとおりです。

contentResolver.openFileDescriptor(item, "w", null).use {
    FileOutputStream(it!!.fileDescriptor).use { outputStream ->
        val imageInputStream = resources.openRawResource(R.raw.my_image) // getting XML
        while (true) {
            val data = imageInputStream.read()
            if (data == -1) {
                break
            }
            outputStream.write(data)
        }
        imageInputStream.close()
        outputStream.close()
    }
}

UriからFDを取得するときに ContentResolver.openFileDescriptor()を使用します。 私はアプリの /res/raw/my_image.jpgパスにイメージファイルを入れて置き、これをFDにwriteくれました。

IS_PENDING = 0, アップデート

すべてのファイルをwriteた場合、 IS_PENDINGプロパティを0に変更してくれるとします。その後、他のアプリでは、このファイルを使用することができます。

values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(item, values, null, null)

ファイルが保存されている位置

上記のコードでInsert()の引数としてMediaStore.VOLUME_EXTERNAL_PRIMARYを伝えました。

保存されたファイルの実際のパスをshellで確認してみると次のとおりです。

generic_x86:/sdcard/Pictures $ ls
my_image_q.jpg

もしパスを変更してくれたいContentValuesオブジェクトに RELATIVE_PATH属性にパスを設定しヘジュオヤます。

val values = ContentValues().apply {
    put(MediaStore.Audio.Media.RELATIVE_PATH, "DCIM/My Images")
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image_q2.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

上記のように保存すると、RELATIVE_PATHに設定され "/DCIM/My Images/"の下にファイルが保存されます。

generic_x86:/sdcard/DCIM/My Images $ ls
my_image_q2.jpg

Imagesの場合は、以下二つのパスのみのデータを保存することができます。

  • /DCIM
  • /Pictures

別のパスを設定すると、次のようにエラーが発生し保存されません。

java.lang.IllegalArgumentException: Primary directory DCIM1 not allowed for content://media/external_primary/images/media; allowed directories are [DCIM, Pictures]
    at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:170)

MediaStoreにInsertする方法(Android P以下)

P以下では、Qと少し異なります。まずMediaStoreにwriteする WRITE_EXTERNAL_STORAGE権限が必要です。

以下は、Pから画像ファイルをMediaStoreに登録する例です。

// copy /res/raw/my_image.jpg to /data/data/[app package]/files/my_image.jpg
val inputStream = resources.openRawResource(R.raw.my_image)
val filePath = "$filesDir/my_image.jpg"
val outputStream = FileOutputStream(filePath)
while (true) {
    val data = inputStream.read()
    if (data == -1) {
        break
    }
    outputStream.write(data)
}
inputStream.close()
outputStream.close()

// Insert my file to MediaStore
val values= ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image6.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.DATA, filePath)
}
val item = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!

このコードは、次の操作を実行します。

  1. /res/raw/に保存されたファイルをアプリのファイルフォルダにコピーします。
  2. コピーしたファイルをMediaStoreに保存します。

次のコードは、MediaStoreに画像ファイルを保存するためのコードであるが、直接writeせず DATA属性に絶対パスを設定すると、世話を保存します。 (こちらについて関心がなく、他の方法でも、ファイルをInsertすることができるたしれませんね)

val values= ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image6.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.DATA, filePath)
}
val item = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!

ファイルの絶対パスが必要なため、次のようにrawに保存された画像ファイルをアプリのファイルフォルダに保存することができしかありませんでした。

val inputStream = resources.openRawResource(R.raw.my_image)
val filePath = "$filesDir/my_image.jpg"
val outputStream = FileOutputStream(filePath)
while (true) {
    val data = inputStream.read()
    if (data == -1) {
        break
    }
    outputStream.write(data)
}
inputStream.close()
outputStream.close()

Video、AudioファイルをMediaStoreにInsert

ContentValuesに設定する属性がImagesではなく VideoAudioということだけ違って、他のコードは、同じです。

まとめ

QでScoped Storageが導入されREAD / WRITE EXTERNAL STORAGE権限が無意味になりました。 MediaStoreでファイルを読むときREAD権限を要求するが、書くときWRITE権限を必要としません。 ただし一定のフォルダにのみ書き込むことができます。

Googleの意図通り、Scoped StorageをサポートしているデバイスからメディアファイルはMediaStoreを利用してアクセスすることが良さそうです。 しかし、それはまだQデバイスが多く出ておらず、いくつかの不快感(またはバグ)があるかわからないので注意を払いながら開発と良さそうです。

この記事で使用されたサンプルコードは、GitHub - MediaStoreを参照してください。

参考

Related Posts

codechachaCopyright ©2019 codechacha