Android 11 Scoped Storage
Android 11以降のストレージアクセスに関する包括的なガイド
問題の概要
Android 11(APIレベル30)では、Scoped Storage(スコープ付きストレージ)の導入により、外部ストレージへのアクセス方法が大きく変更されました。これまでEnvironment.getExternalStorageDirectory()
を使用してファイルパスを直接アクセスしていたアプリケーションは、Android 11では動作しない可能性があります。
主な変更点:
- アプリ固有のディレクトリ以外への直接アクセス制限
- メディアファイルと非メディアファイルのアクセス権限の分離
MANAGE_EXTERNAL_STORAGE
権限の導入とその制約
ソリューション
1. 基本となる権限設定
Android 11に対応するためのマニフェスト設定:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- 注意: MANAGE_EXTERNAL_STORAGEは慎重に使用 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
WARNING
MANAGE_EXTERNAL_STORAGE
権限は、ファイルマネージャーやウイルス対策アプリなど、正当な理由がある場面でのみ使用してください。この権限を要求するアプリはGoogle Playストアでの審査が厳しくなります。
2. 権限の確認とリクエスト
権限状態を確認する方法:
private fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
val readResult = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val writeResult = ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
readResult == PackageManager.PERMISSION_GRANTED &&
writeResult == PackageManager.PERMISSION_GRANTED
}
}
権限をリクエストする方法:
private fun requestStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.data = Uri.parse("package:${packageName}")
startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE)
} catch (e: Exception) {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivityForResult(intent, REQUEST_CODE_MANAGE_STORAGE)
}
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
REQUEST_CODE_STORAGE
)
}
}
3. メディアファイルへのアクセス
MediaStoreを使用した画像ファイルの取得:
data class ImageInfo(
val uri: Uri,
val name: String,
val size: Long
)
fun loadImagesFromGallery(): List<ImageInfo> {
val imageList = mutableListOf<ImageInfo>()
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
contentResolver.query(
collection,
projection,
null,
null,
sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val size = cursor.getLong(sizeColumn)
val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
imageList.add(ImageInfo(contentUri, name, size))
}
}
return imageList
}
4. アプリ固有ストレージの使用
アプリ専用ディレクトリの使用が推奨されます:
// 画像保存用ディレクトリ
val imagesDir = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "my_app")
if (!imagesDir.exists()) {
imagesDir.mkdirs()
}
// ファイルの保存
val imageFile = File(imagesDir, "photo_${System.currentTimeMillis()}.jpg")
try {
FileOutputStream(imageFile).use { outputStream ->
// 画像データを書き込む
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
}
} catch (e: IOException) {
e.printStackTrace()
}
5. SAF(Storage Access Framework)の使用
ユーザーにファイル選択を促す方法:
private fun createFile(pickerInitialUri: Uri) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/jpeg"
putExtra(Intent.EXTRA_TITLE, "my_image.jpg")
putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
startActivityForResult(intent, REQUEST_CODE_CREATE_FILE)
}
ベストプラクティス
TIP
ターゲットSDKバージョンの選択: 段階的な移行のために、一時的にターゲットSDKを29に設定することも選択肢ですが、長期的にはAndroid 11への対応が必要です。
権限の最小限化:
MANAGE_EXTERNAL_STORAGE
は最後の手段として考え、まずはMediaStoreやSAFを使用するようにしましょう。後方互換性の確保: バージョンごとに分岐処理を実装し、すべてのAndroidバージョンで動作するようにします。
ユーザーエクスペリエンス: 権限リクエスト時には、なぜその権限が必要なのかユーザーに明確に説明しましょう。
トラブルシューティング
よくある問題と解決策:
ファイルパスが機能しない: 直接パスを使用する代わりに、ContentResolverとURIを使用してください。
権限が拒否される:
MANAGE_EXTERNAL_STORAGE
が必要な場合、ユーザーを設定画面に誘導する必要があります。カメラアプリ連携の問題: Android 11では特定のカメラアプリへの明示的なIntentが必要になる場合があります。
結論
Android 11のScoped Storageはユーザーのプライバシー保護を強化するための重要な変更です。従来のファイル直接アクセスから、MediaStoreやSAFを使用した現代的なアプローチに移行することが重要です。適切な権限管理とユーザーへの明確な説明により、Android 11でも快適に動作するアプリを開発できます。
詳細については、Android公式ドキュメントを参照してください。