パブリックな共有ストレージのMediaStoreに画像を保存する方法について見てみましょう。
2024.1.1
MediaStore
ストレージには内部ストレージ(Internal Storage)と外部ストレージ(External Storage)があります。アプリには固有のストレージ領域がありますが、共有のストレージ領域を使うことも可能です。
メディアコレクションは共有可能なストレージで写真、動画やミュージックなどのメディアを保存できる領域です。
- MediaStore APIを使ってアクセスし、他のアプリが作成したメディアには読み出しでアクセスできる
- 読み出しにはREAD_EXTERNAL_STORAGE のPermissionが必要,
- 保存に WRITE_EXTERNAL_STORAGE は不要(Android 10 以降)
- アプリが削除されてもファイルは残っている
● 画像:(写真とスクリーンショットを含む)
DICM/、Picture/ ディレクトリに格納されている
● 動画:
DCIM/、Movies/、Pictures/ ディレクトリに格納
● オーディオ:
オーディオファイルは Alarms/、Audiobooks/、Music/、Notifications/、Podcasts/、Ringtones/ ディレクトリに格納
オーディオ プレイリストは Music/ ディレクトリまたは Movies/ ディレクトリに格納
プロバイダにデータを挿入
Mediaファイルを保存するには、ContentResolver.insert() を使ってプロバイダにデータを挿入します。プロバイダに新しい行を挿入し、その行のコンテンツ URI を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Defines a new Uri object that receives the result of the insertion lateinit var newUri: Uri ... // Defines an object to contain the new values to insert val newValues = ContentValues().apply { /* * Sets the values of each column and inserts the word. The arguments to the "put" * method are "column name" and "value" */ put(UserDictionary.Words.APP_ID, "example.user") put(UserDictionary.Words.LOCALE, "en_US") put(UserDictionary.Words.WORD, "insert") put(UserDictionary.Words.FREQUENCY, "100") } newUri = contentResolver.insert( UserDictionary.Words.CONTENT_URI, // the user dictionary content URI newValues // the values to insert ) |
ContentValues:ContentResolverにデータを保存するためのクラスで、
ファイル情報を入力し、insert() の引数に使います
insert()からUriオブジェクトが返されるので、そこに保存します。
IS_PENDING を 1 に設定して処理が完了していないので他からのアクセスを無視するようにします。
処理が終わったら IS_PENDING を 0 にして解除してから
update() にて更新をします。
サンプルコード
drawableに入れた画像をMediaStoreに保存してみましょう。
MainActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
//package com.example.kotlinmediastore import android.content.ContentValues import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle import android.os.Environment import android.provider.MediaStore import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import java.io.IOException class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } var newUri : Uri val textView : TextView = findViewById(R.id.text_view) val imageView : ImageView = findViewById(R.id.image_view) val bmp: Bitmap = BitmapFactory.decodeResource(resources, R.drawable.sample_image) val button : Button = findViewById(R.id.button) button.setOnClickListener { if(isExternalStorageWritable()) { val values = ContentValues() // コンテンツ クエリの列名 // ファイル名 values.put(MediaStore.Images.Media.DISPLAY_NAME, "TestSampleImage.jpg") // マイムの設定 values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") // 書込み時にメディア ファイルに排他的にアクセスする values.put(MediaStore.Images.Media.IS_PENDING, 1) val resolver = applicationContext.contentResolver val collection = MediaStore.Images.Media.getContentUri( MediaStore.VOLUME_EXTERNAL_PRIMARY ) newUri = resolver.insert(collection, values)!! try { contentResolver.openOutputStream(newUri).use { outstay -> checkNotNull(outstay) bmp.compress(Bitmap.CompressFormat.JPEG, 70, outstay) textView.setText(R.string.saved) imageView.setImageBitmap(bmp) } } catch (e: IOException) { throw RuntimeException(e) } values.clear() // 排他的にアクセスの解除 values.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(newUri, values, null, null) } } } /* Checks if external storage is available for read and write */ private fun isExternalStorageWritable(): Boolean { val state = Environment.getExternalStorageState() return Environment.MEDIA_MOUNTED == state } } |
適当な画像をsample_image.jpgとして用意しdrawableに入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="10dp" android:text="@string/text" android:textColor="#000" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.05" /> <Button android:id="@+id/button" android:text="@string/button" android:layout_margin="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.1" /> <ImageView android:id="@+id/image_view" android:scaleType="centerInside" android:contentDescription="@string/description" android:layout_margin="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.7" /> </androidx.constraintlayout.widget.ConstraintLayout> |
strings.xml
1 2 3 4 5 6 7 |
<resources> <string name="app_name">KotlinMediaStore</string> <string name="description">a picture</string> <string name="button">Save</string> <string name="saved">Saved Image</string> <string name="text">Test</string> </resources> |
これで実行すると、
ボタンタップで画像が保存される
スマホの「フォト」アプリから共有ストレージを見ると「Pictures」に保存されているのがわかります。
画像を選択タップする
画像の情報を見ると
以下に保存されていることが分かります。
1 |
/storage/emulated/0/Pictures/TestSampleImage.jpg |
尚、このパスはあくまで例で実装は端末に依存します。
関連ページ:
References:
外部ストレージ内のメディア ファイルを操作する
共有ストレージからメディア ファイルにアクセスする
コンテンツ プロバイダの基本