Skip to content

Android 13で外部ストレージに書き込む方法

重要

Android 11(APIレベル30)以降、WRITE_EXTERNAL_STORAGE権限は外部ストレージへのフルアクセスを提供しません。Android 13(APIレベル33)では、この権限に対するランタイム許可をリクエストしてもダイアログは表示されません。

問題の本質

Android 13では:

  1. WRITE_EXTERNAL_STORAGE権限リクエストが無効化されている
  2. 外部ストレージ全体への書き込みアクセスが禁止された
  3. 代わりにアプリ固有ストレージの使用が推奨される
  4. メディアファイルには新しい細分化された権限が必要

推奨ソリューション

方法1: アプリ固有の外部ストレージを使用する(最適解)

権限不要で直接アクセス可能。ユーザーの端末上に残留ファイルが残らない優れた設計。

kotlin
val appSpecificDir = getExternalFilesDir("Keystore")
val file = File(appSpecificDir, "my_keystore.jks")

try {
    FileOutputStream(file).use { stream ->
        // キーストアデータ書き込み
    }
} catch (e: IOException) {
    e.printStackTrace()
}
java
File directory = getExternalFilesDir("Keystore");
File file = new File(directory, "my_keystore.jks");

try (FileOutputStream stream = new FileOutputStream(file)) {
    // キーストアデータ書き込み
} catch (IOException e) {
    e.printStackTrace();
}

特徴

  • アプリ削除時にファイルも自動削除
  • APIレベル19(Android 4.4)以降で動作
  • AndroidManifest.xmlに権限宣言不要

方法2: ストレージアクセスフレームワーク(SAF)を使用する

すべてのAndroidバージョンで動作し、共有ストレージ領域へのアクセスを可能にするシステム標準の方法。

kotlin
// Step 1: ディレクトリ選択リクエスト
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_CODE_DIRECTORY)

// Step 2: 選択結果の処理
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_DIRECTORY && resultCode == RESULT_OK) {
        data?.data?.let { uri ->
            contentResolver.takePersistableUriPermission(
                uri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION or 
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            )
            // 権限を永続化
            saveUriToPreferences(uri)
            
            // ファイル作成
            val directory = DocumentFile.fromTreeUri(this, uri)
            val file = directory?.createFile("application/jks", "my_keystore.jks")
            contentResolver.openOutputStream(file?.uri)?.use { stream ->
                // キーストアデータ書き込み
            }
        }
    }
}

方法3: メディアファイルの場合(該当時)

画像/動画/音声ファイルにアクセスする必要がある場合のみ:

xml
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

注意

READ_MEDIA_*権限はメディアコンテンツ専用です。キーストアファイルなどの非メディアデータには適用できません。

アンチパターンと注意点

WRITE_EXTERNAL_STORAGEを使用する

kotlin
// Android 13では動作しない
ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
    REQUEST_CODE
)

重大

MANAGE_EXTERNAL_STORAGE権限(全ファイルアクセス権):

  • Google Playポリシーで制限されている
  • ファイルマネージャーやバックアップツール等の特定アプリのみ許可
  • 通常アプリでの使用は審査拒否の原因

バージョン分岐の誤用例

権限チェック方法のバージョン分岐時に起こりやすい間違い:

kotlin
// 間違った実装例(API 33では常にfalse)
fun isStoragePermissionGranted(): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED
    } else {
        ...
    }
}

正しい検出方法

アプリ固有ストレージには権限不要のため、権限チェック自体が不要。SAFではURIパーミッションの有効性を確認する。

設計指針

  1. ユーザーデータ → アプリ固有ストレージ
  2. 他アプリ共有ファイル → SAF経由
  3. メディアファイル → READ_MEDIA_*権限
  4. 共有ストレージへの直接アクセスは不可避な場合のみSAF利用

バージョン対応例

Android 10以降に対する安全な実装:

kotlin
fun saveKeystoreFile(data: ByteArray) {
    val file = when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
            File(getExternalFilesDir("Keystore"), "secure.jks")
        }
        else -> {
            // Android 9以下:従来の権限チェック
            if (checkStoragePermission()) {
                File(Environment.getExternalStorageDirectory(), "app_name/secure.jks")
            } else {
                // エラーハンドリング
                return
            }
        }
    }
    
    file.outputStream().use { it.write(data) }
}

まとめ

Android 13での外部ストレージ操作は以下のいずれかで実装:

方法用途必要な権限
アプリ固有ストレージプライベートファイル不要
ストレージアクセスフレームワーク共有ファイルユーザー選択
READ_MEDIA_*メディアファイルランタイム許可

キーストアファイルのようなプライベートデータ保存には、アプリ固有ストレージが最も安全で最適なソリューションです。