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)!!
このコードは、次の操作を実行します。
- /res/raw/に保存されたファイルをアプリのファイルフォルダにコピーします。
- コピーしたファイルを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ではなく Video
、Audio
ということだけ違って、他のコードは、同じです。
まとめ
QでScoped Storageが導入されREAD / WRITE EXTERNAL STORAGE権限が無意味になりました。 MediaStoreでファイルを読むときREAD権限を要求するが、書くときWRITE権限を必要としません。 ただし一定のフォルダにのみ書き込むことができます。
Googleの意図通り、Scoped StorageをサポートしているデバイスからメディアファイルはMediaStoreを利用してアクセスすることが良さそうです。 しかし、それはまだQデバイスが多く出ておらず、いくつかの不快感(またはバグ)があるかわからないので注意を払いながら開発と良さそうです。
この記事で使用されたサンプルコードは、GitHub - MediaStoreを参照してください。
参考
Related Posts
- Android - FusedLocationProviderClientに位置情報を取得する
- Android - GPS、Network位置情報を取得する(LocationManager)
- Android - adbコマンドでActivity実行
- アンドロイド - MediaStoreにメディアファイルを保存する方法
- Android - Runtime permissionリクエスト方法と例(kotlin)
- Android11 - Storage(ストレージ)の変更まとめ
- Jetpack Compose - RowとColumn
- Android 13 - 細かいメディアファイルの権限
- Android 13でNotification権限をリクエスト、通知を表示する
- エラー解決:android gradle plugin requires java 11 to run. you are currently using java 1.8.
- Query method parameters should either be a type that can be converted into a database column or a List
- Android - TabLayoutの実装方法(+ ViewPager2)
- Android - adbコマンドで特定のパッケージのプロセスの終了
- Android - adb push、pullでファイルのコピー、ダウンロード
- Android - adbコマンドでsettings value確認、変更、
- Android 12 - IntentFilterのexported明示的な宣言
- Android - adb logcatコマンドでログ出力
- Android - ACTION_BOOT_COMPLETEDイベント受信
- Android - Foreground Service実行
- Android - ファイル入出力の例(Read、Write、内部、外部ストレージ)
- Android - アプリの権限を確認(Permission check)
- Android - adbで実行中のプロセス、スレッドリスト及びメモリ情報の確認
- Android - Broadcast Receiver登録およびイベントの受信方法
- Android - Cleartext HTTP ... not permitted例外解決方法
- Androidのビルドエラー - Calls to Java default methods are prohibited in JVM target 1.6
- アンドロイド - Assetsでファイルを読み取る方法
- アンドロイドのさまざまなNotification種類と実装方法
- アンドロイド - INSTALL_FAILED_TEST_ONLYエラー解決方法
- Android EspressoのCustom Matcher実装方法
- Android Espressoを使用してUIをテストする方法(3)
- アンドロイド - CTS hostsideをgradleで構築する方法
- Androidのアプリのデータフォルダーのパスと内部/外部ストレージ説明
- アンドロイド - 最初のApp作成
- Androidをインストールする方法(Windows)