Android 14におけるメディアストレージ更新エラーと対応策

あるアプリケーションで、以下のコードが Android 14(API 34)環境でクラッシュする問題が発生しました:

val values = ContentValues().apply {
    put(MediaStore.Images.Media.DATA, file.absolutePath)
    put(MediaStore.Images.Media.DISPLAY_NAME, file.name)
}
contentResolver.update(uri, values, null, null)

エラーメッセージは Mutation of _data is not allowed.。この問題の背景には、Android のストレージ権限モデルの進化があります。

ストレージアクセスの変遷

Android 6.0 以降、危険な権限は実行時にユーザーに確認が必要となりました。さらに Android 10 では「スコープストレージ」が導入され、アプリは自身のサンドボックス領域外への直接アクセスが制限されました。

Android 13 では READ_EXTERNAL_STORAGE 権限が細分化され、代わりに以下のような専用権限が導入されています:

  • READ_MEDIA_IMAGES
  • READ_MEDIA_VIDEO
  • READ_MEDIA_AUDIO

マニフェストでの互換性確保のため、以下のように宣言します:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
                 android:maxSdkVersion="32" />

MediaStore API の正しい使い方

画像をメディアライブラリに保存する場合、DATA カラム(絶対パス)の指定は不要です。代わりに相対パスやURIベースの操作を行います:

fun saveImageToGallery(context: Context, bitmap: Bitmap, filename: String) {
    val contentValues = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, filename)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
        }
    }

    val resolver = context.contentResolver
    val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)

    imageUri?.let { uri ->
        resolver.openOutputStream(uri)?.use { stream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
        }
    }
}

ファイル選択の代替手段

非メディアファイル(PDFなど)を扱う場合は、システムファイルピッカーを使用します:

val filePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode == RESULT_OK) {
        result.data?.data?.let { uri ->
            contentResolver.openInputStream(uri)?.use { inputStream ->
                // ファイル処理
            }
        }
    }
}

fun launchFilePicker() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        type = "*/*"
        addCategory(Intent.CATEGORY_OPENABLE)
    }
    filePicker.launch(intent)
}

特殊権限 MANAGE_EXTERNAL_STORAGE

ファイルマネージャーやバックアップツールなど、全ファイルアクセスが必要なアプリ向けに MANAGE_EXTERNAL_STORAGE 権限が存在しますが、Google Play の審査基準により一般的なアプリでは使用できません。

権限チェック方法:

if (Environment.isExternalStorageManager()) {
    // 全ファイルアクセス可能
} else {
    // 設定画面へ誘導
    val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
    startActivity(intent)
}

根本的な解決策

冒頭のクラッシュは、MediaStore.Images.Media.DATA を更新しようとしたことが原因です。Android 14 からはこのカラムの更新が禁止されています。解決策は単純で、該当行を削除することです:

// 削除すべき行
// values.put(MediaStore.Images.Media.DATA, file.absolutePath)

// 必要な情報のみ設定
values.put(MediaStore.Images.Media.DISPLAY_NAME, file.name)
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}

この修正により、最新のストレージガイドラインに準拠しつつ、安定した動作が得られます。

タグ: Android MediaStore ストレージ権限

6月27日 21:37 投稿