AVDや手持ちの実機では、従来の方法(pathからFileOutputStream生成)でアクセスできるから、気づかなかったよ
一部の実機では、従来の方法では保存も読み込みもできないわ
パーミッション
AndroidManifest.xml で、READ_EXTERNAL_STORAGE権限、WRITE_EXTERNAL_STORAGE権限を宣言しておきます。
また、パーミッションダイアログを表示するコードを実装しておきます。(この記事では省略します)
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Code language: HTML, XML (xml)
android:maxSdkVersion="28"について
https://developer.android.com/training/data-storage/shared/media?hl=ja を見ると、ターゲットがAndroid10以上の場合、android:maxSdkVersion="28"を指定するようにとあります。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Code language: HTML, XML (xml)
ところが、この指定をすると、ターゲットがAndroid10、実行デバイスがAVDのAndroid10で、新規保存しようとすると、Permission不足で書き込めませんでした。
そのため、ターゲットがAndroid10でも、android:maxSdkVersion="28"を指定せず、パーミッションダイアログを表示することにしました。
筆者の実装コードがよくないのかもしれないから、鵜呑みにしないでね
新規保存
Bitmap bmpをString pathに保存する例です。
pathは従来と同じように組み立てます。例えば、/storage/emulated/0/Pictures/MyApp/foo.jpg です。
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/MyApp/foo.jpg";
Code language: Java (java)
保存処理をクラスやメソッドに分けることを想定しています。ContentResolverはコンストラクタで渡し、Bitmap bmpとString pathはメソッドの引数で渡します。
pathその他のデータをContetResolverに登録して、imageUriを取得します。
SDK_INT >= 29の場合は、RELATIVE_PATHを設定して、IS_PENDING を オンにします。
final String name = new File(path).getName();
final ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, name);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATA, path);
if (Build.VERSION.SDK_INT >= 29) {
final String relativeDir = getRelativeDir(path);
values.put(MediaStore.Images.Media.RELATIVE_PATH, relativeDir);
values.put(MediaStore.Images.Media.IS_PENDING, true);
}
final Uri externalStorageUri = getExternalStorageUri(Build.VERSION.SDK_INT);
Uri imageUri = contentResolver.insert(externalStorageUri, values);
Code language: Java (java)
public Uri getExternalStorageUri(int sdk_int) {
if (sdk_int < 29) {
return MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else {
return MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
}
}
Code language: Java (java)
path | /storage/emulated/0/Pictures/MyApp/foo.jpg |
RELATIVE_PATH | Pictures/MyApp |
pathが、/storage/emulated/0/Pictures/MyApp/foo.jpg なら、
RELATIVE_PATHは、/storage/emulated/0からの相対パス Pictures/MyApp です。RELATIVE_PATHに foo.jpgは含めません。
ディレクトリ Pictures/MyApp が存在しなくても、自前でmkdirする必要はありません。contentResolver.insert内で、自動でmkdirされます。
public String getRelativeDir(final String path) {
final String target = Environment.getExternalStoragePublicDirectory("").getPath() + "/";
final String relativePath = path.replace(target, "");
final File file = new File(relativePath);
final String relativeDir = file.getParent();
return relativeDir;
}
Code language: Java (java)
imageUriからOutputStreamを取得して、Bitmap.compressで保存します。
final OutputStream outputStream = contentResolver.openOutputStream(imageUri);
bmp.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
Code language: Java (java)
SDK_INT >= 29の場合は、最後に IS_PENDING を オフにします。
if (Build.VERSION.SDK_INT >= 29) {
final ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.IS_PENDING, false);
contentResolver.update(imageUri, values, null, null);
}
Code language: Java (java)
削除
ContentResolverのdeleteメソッドで削除します。ContentResolverからデータが削除されて、画像ファイル自体も削除されます。
public void delete(final long id) {
final Uri externalStorageUri = getExternalStorageUri(Build.VERSION.SDK_INT);
final String selection = "_id = ?";
final String[] selectionArgs = {
String.valueOf(id)
};
contentResolver.delete(externalStorageUri, selection, selectionArgs);
}
public void delete(final String path) {
final Uri externalStorageUri = getExternalStorageUri(Build.VERSION.SDK_INT);
final String selection = "_data = ?";
final String[] selectionArgs = {
path
};
contentResolver.delete(externalStorageUri, selection, selectionArgs);
}
Code language: Java (java)
Bitmapの読み込み
FileDescriptorバージョンです。
ContentResolverのidから、画像uriを作ります。
画像uriからFileDescriptorを取得します。
BitmapFactory.OptionsのinSampleSizeなどを指定して、FileDescriptorでBitmapを読み込みます。
final Uri externalStorageUri = getExternalStorageUri(Build.VERSION.SDK_INT);
final imageUri = ContentUris.withAppendedId(externalContentUri, id);
final FileDescriptor fd = context.getContentResolver().openFileDescriptor(imageUri, "r").getFileDescriptor();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 4;
Bitmap bmp = BitmapFactory.decodeFileDescriptor(fd, null, opts);
Code language: Java (java)
InputStreamバージョンです。
画像uriからInputStreamを取得します。
InputStreamでBitmapを読み込みます。
final Uri externalStorageUri = getExternalStorageUri(Build.VERSION.SDK_INT);
final imageUri = ContentUris.withAppendedId(externalContentUri, id);
final InputStream inputStream = context.getContentResolver().openInputStream(imageUri);
Bitmap bmp = BitmapFactory.decodeStream(inputStream);
Code language: Java (java)
関連記事
このセクションでは、プライバシーに関連する Android 10 の主な変更点をご紹介します。
https://developer.android.com/about/versions/10/privacy/changes?hl=ja#scoped-storage
アプリのファイルとメディアを対象とする外部ストレージ アクセス
デフォルトでは、Android 10 以降をターゲットとするアプリには外部ストレージに対するスコープ アクセス、つまり対象範囲別ストレージが付与されます。
外部ストレージからアクセスする
https://developer.android.com/training/data-storage/app-specific?hl=ja#external
共有ストレージからメディア ファイルにアクセスする
https://developer.android.com/training/data-storage/shared/media?hl=ja