Skip to content

Android 11 Scoped Storage

Android 11以降のストレージアクセスに関する包括的なガイド

問題の概要

Android 11(APIレベル30)では、Scoped Storage(スコープ付きストレージ)の導入により、外部ストレージへのアクセス方法が大きく変更されました。これまでEnvironment.getExternalStorageDirectory()を使用してファイルパスを直接アクセスしていたアプリケーションは、Android 11では動作しない可能性があります。

主な変更点:

  • アプリ固有のディレクトリ以外への直接アクセス制限
  • メディアファイルと非メディアファイルのアクセス権限の分離
  • MANAGE_EXTERNAL_STORAGE権限の導入とその制約

ソリューション

1. 基本となる権限設定

Android 11に対応するためのマニフェスト設定:

xml
<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. 権限の確認とリクエスト

権限状態を確認する方法:

kotlin
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
    }
}

権限をリクエストする方法:

kotlin
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を使用した画像ファイルの取得:

kotlin
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. アプリ固有ストレージの使用

アプリ専用ディレクトリの使用が推奨されます:

kotlin
// 画像保存用ディレクトリ
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)の使用

ユーザーにファイル選択を促す方法:

kotlin
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

  1. ターゲットSDKバージョンの選択: 段階的な移行のために、一時的にターゲットSDKを29に設定することも選択肢ですが、長期的にはAndroid 11への対応が必要です。

  2. 権限の最小限化: MANAGE_EXTERNAL_STORAGEは最後の手段として考え、まずはMediaStoreやSAFを使用するようにしましょう。

  3. 後方互換性の確保: バージョンごとに分岐処理を実装し、すべてのAndroidバージョンで動作するようにします。

  4. ユーザーエクスペリエンス: 権限リクエスト時には、なぜその権限が必要なのかユーザーに明確に説明しましょう。

トラブルシューティング

よくある問題と解決策:

  1. ファイルパスが機能しない: 直接パスを使用する代わりに、ContentResolverとURIを使用してください。

  2. 権限が拒否される: MANAGE_EXTERNAL_STORAGEが必要な場合、ユーザーを設定画面に誘導する必要があります。

  3. カメラアプリ連携の問題: Android 11では特定のカメラアプリへの明示的なIntentが必要になる場合があります。

結論

Android 11のScoped Storageはユーザーのプライバシー保護を強化するための重要な変更です。従来のファイル直接アクセスから、MediaStoreやSAFを使用した現代的なアプローチに移行することが重要です。適切な権限管理とユーザーへの明確な説明により、Android 11でも快適に動作するアプリを開発できます。

詳細については、Android公式ドキュメントを参照してください。