jiichan.com

PROGRAMMING

Kotlin
javascript
PHP
Java

jetpack compose 外部(共有)ストレージの Media ファイルへのアクセス

androidで、端末内にファイルを保存したり読み込んだりする方法がよく分からなかったので整理してみました。
特に、内部ストレージ、外部ストレージ、アプリ固有のストレージ、共有ストレージ、メディア用途、その他の用途、 と種類が多く分かりずらいです。
また、Api のバージョンで permission の要・不要も変わります。
何回かに分けてストレージへのアクセス方法を確認してみます。

≪開発環境≫
windows11
andriod studio koala
≪実機デバッグ≫
android13

ストレージの構成

google のサイトなど色々調べ、大まかに分けてみたのが次の表です。

区分1区分2区分3アクセス
内部ストレージアプリ固有のストレージ自分のみOK
外部ストレージアプリ固有のストレージ
Android11以降作成不可
自分のみOK
共有ストレージメディア用途他のアプリからもOK
メディア以外用途他のアプリからもOK

他のアプリで作られたファイル (LINEの画像など) にアクセスする以外は、パーミッションは不要のようです。

imageファイルへの書き込み

書き込み 方法1


// 外部ストレージ(共有ストレージ)に書き込み(Media) Direct ============================
val context = LocalContext.current

Button(
	onClick = {
		// 保存されるコンテンツURI(DCIM, Pictures ディレクトリ = MediaStoreのテーブル)
		val collection = if (Build.VERSION.SDK_INT >= 29) {
			MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
		} else {
			MediaStore.Images.Media.EXTERNAL_CONTENT_URI
		}
		// 保存されるファイルの情報
		val contentValues = ContentValues().apply {
			put(MediaStore.Images.Media.DISPLAY_NAME, "image2.jpg")	// このファイル名で保存
			put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")		// image/png
			put(MediaStore.Images.Media.IS_PENDING, true)
			// 保存場所に専用のフォルダを作成する場合
//          put(
//              MediaStore.Images.ImageColumns.RELATIVE_PATH,
//              Environment.DIRECTORY_PICTURES + "/MY_PICTURES/")
		}

		// 登録されたファイルを指しているUriオブジェクト.このUriにデータwriteを行うことができる
		val contentUri = contentResolver.insert(collection, contentValues)

		// セットされたUriにファイルを書き込む
		contentResolver.openFileDescriptor(contentUri!!, "w", null).use {
			FileOutputStream(it!!.fileDescriptor).use { outputStream ->
				val resources = context.resources
				// コピー元の画像
				val imageInputStream = resources.openRawResource(R.raw.image2)
				while (true) {
					val data = imageInputStream.read()
					if (data == -1) {
						break
					}
					outputStream.write(data)
				}
				imageInputStream.close()
				outputStream.close()
			}
		}

		contentValues.clear()
		if (Build.VERSION.SDK_INT >= 29) {
			contentResolver.update(contentUri, contentValues.apply {
				put(MediaStore.Images.Media.IS_PENDING, false)
			}, null, null)
		} else {
			contentResolver.update(contentUri, contentValues, null, null)
		}

		Toast.makeText(context, "ファイルが作成されました。", Toast.LENGTH_SHORT).show()
	}
) {
	Text(text = "共有(Media)書き込み/Direct")
}

書き込み 方法2


// 外部ストレージ(共有ストレージ)から読み込み(Media) Direct ==========================
// 他のアプリが作成した画像にはパーミッションが必要
val context = LocalContext.current

Button(
	onClick = {
		// DCIM, Pictures ディレクトリ MediaStoreのテーブル
		val collection = if (Build.VERSION.SDK_INT >= 29) {
			MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
		} else {
			MediaStore.Images.Media.EXTERNAL_CONTENT_URI
		}

		// 読み込む列の指定。nullならすべての列を読み込む
		val projection = arrayOf(
			MediaStore.Images.Media._ID,            // URIそのものの取得に必要
			MediaStore.Images.Media.DISPLAY_NAME,   // 拡張子が付いたファイル名
			MediaStore.Images.Media.SIZE            // ファイルサイズ
		)
		// 行の絞り込みの指定.nullならすべての行を読み込む(where句)
		val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?"
		// selectionの?を置き換える引数
		val selectionArgs = arrayOf("image2.jpg")   // ファイル名を指定しているの一つだけ取得
		// 並び順。nullなら指定なし "${MediaStore.Images.Media.DATE_ADDED} DESC"
		val sortOrder = null

		contentResolver.query(
			collection, projection, selection, selectionArgs, sortOrder
		)?.use { cursor ->
			// 必要な情報が格納されている列番号を取得する、0から始まるインデックスで返す
			val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)

			// 複数ファイル取得の場合。順にカーソルを動かしながら、情報を取得していく
//          while (cursor.moveToNext()) {
//              val id = cursor.getLong(idColumn)
//              // IDからURIを取得して
//              val uri = ContentUris.withAppendedId(collection, id)
                // uri ごとに色々処理する
//              uris.add(uri)                 // 処理例1 リストに格納
//              uri.getBitmapOrNull(context.contentResolver)?.let { bitmap: Bitmap ->
//                 imageBitmap = bitmap      // 処理例2 ただ上書きしてるだけ
//              }
//          }

			// 単一ファイル取得の場合
			cursor.moveToFirst()
			val id = cursor.getLong(idColumn)
			// IDからURIを取得
			val uri2 = ContentUris.withAppendedId(collection, id)
			// coil で表示するか bitmap にして表示する
			uri2.getBitmapOrNull(context.contentResolver)?.let { bitmap: Bitmap ->
				imageBitmap = bitmap
			}
		}
	}
) {
	Text(text = "共有(Media)読み込み/Direct")
}

imageファイルからの読み込み

読み込み 方法1


// 外部ストレージ(共有ストレージ)から読み込み(Media) Picker ===============
val context = LocalContext.current
var imageBitmap by remember { mutableStateOf<Bitmap?>(null) }
val openImgLauncher =
	rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { imageUri: Uri? ->
		imageUri?.getBitmapOrNull(context.contentResolver)?.let { bitmap: Bitmap ->
			// 取得したBitmapのサイズを変更
//          imageBitmap = Bitmap.createScaledBitmap(bitmap, 500, 500, true)
			// サイズをそのまま
			imageBitmap = bitmap
		}
	}

Button(
	onClick = {
		openImgLauncher.launch("image/*")
	}
) {
	Text(text = "共有(Media)読み込み/Picker")
}

読み込み 方法2


// 外部ストレージ(共有ストレージ)から読み込み(Media) Picker ===============
var uri by remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
	uri = it
}

Button(onClick = {
	launcher.launch("image/*")
}) {
	Text("Picker coil")
}

// 画像表示
if (uri != null) {
	Text("uri=${uri}")
	Image(
		painter = rememberImagePainter(uri),    // coil で表示
		contentDescription = null
	)
}

まとめ

方法は何通りかありますが、どれを使うべきなのか爺ちゃんには良くわかりませんでした。