パブリックな共有ストレージの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 |
// Defines a new Uri object that receives the result of the insertion Uri newUri; ... // Defines an object to contain the new values to insert ContentValues newValues = new ContentValues(); // Sets the values of each column and inserts the word. // The arguments to the "put" method are "column name" and "value" newValues.put(UserDictionary.Words.APP_ID, "example.user"); newValues.put(UserDictionary.Words.LOCALE, "en_US"); newValues.put(UserDictionary.Words.WORD, "insert"); newValues.put(UserDictionary.Words.FREQUENCY, "100"); newUri = getContentResolver().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.java
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 86 87 |
package com.example.testmediastore; import android.content.ContentResolver; 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.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import java.io.IOException; import java.io.OutputStream; public class MainActivity extends AppCompatActivity { private TextView textView; private ImageView imageView; private Bitmap bmp; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); setContentView(R.layout.activity_main); ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); return insets; }); textView = findViewById(R.id.text_view); imageView = findViewById(R.id.image_view); bmp = BitmapFactory.decodeResource(getResources(), R.drawable.sample_image); Button button = findViewById(R.id.button); button.setOnClickListener( v -> { if(isExternalStorageWritable()) { ContentValues values = new ContentValues(); // コンテンツ クエリの列名 // ファイル名 values.put(MediaStore.Images.Media.DISPLAY_NAME, "TestSampeImage.jpg"); // マイムの設定 values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); // 書込み時にメディア ファイルに排他的にアクセスする values.put(MediaStore.Images.Media.IS_PENDING, 1); ContentResolver resolver = getApplicationContext().getContentResolver(); Uri collection = MediaStore.Images.Media.getContentUri( MediaStore.VOLUME_EXTERNAL_PRIMARY); Uri item = resolver.insert(collection, values); try { assert item != null; try (OutputStream outstream = getContentResolver().openOutputStream(item)) { assert outstream != null; bmp.compress(Bitmap.CompressFormat.JPEG, 70, outstream); textView.setText(R.string.saved); imageView.setImageBitmap(bmp); } } catch (IOException e) { throw new RuntimeException(e); } values.clear(); // 排他的にアクセスの解除 values.put(MediaStore.Images.Media.IS_PENDING, 0); resolver.update(item, values, null, null); } }); } /* Checks if external storage is available for read and write */ public boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return (Environment.MEDIA_MOUNTED.equals(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">TestMediaStore</string> <string name="description">a picture</string> <string name="button">Save</string> <string name="saved">Saved Image</string> <string name="text">Test</string> </resources> |
これで実行すると、
ボタンタップでSAVE
スマホの「フォト」アプリから共有ストレージを見ると「Pictures」に保存されているのがわかります。
画像をタップ
画像の情報を見ると
以下に保存されていることが分かります。
1 |
/storage/emulated/0/Pictures |
尚、このパスはあくまで例で実装は端末に依存します。
関連ページ:
References:
外部ストレージ内のメディア ファイルを操作する
共有ストレージからメディア ファイルにアクセスする
コンテンツ プロバイダの基本