Android10、Pictures下のファイルはMedia API経由でアクセス

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_PATHPictures/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 の主な変更点をご紹介します。
アプリのファイルとメディアを対象とする外部ストレージ アクセス
デフォルトでは、Android 10 以降をターゲットとするアプリには外部ストレージに対するスコープ アクセス、つまり対象範囲別ストレージが付与されます。

https://developer.android.com/about/versions/10/privacy/changes?hl=ja#scoped-storage

外部ストレージからアクセスする

https://developer.android.com/training/data-storage/app-specific?hl=ja#external

共有ストレージからメディア ファイルにアクセスする

https://developer.android.com/training/data-storage/shared/media?hl=ja
タイトルとURLをコピーしました