Scoped Storage は、Android 10(API 29)で導入されたファイルシステムアクセスモデルで、デバイスの共有ストレージへのアプリの無制限なアクセスを制限します。Google Android Developer Documentation(2024)によると、Scoped Storage は READ_EXTERNAL_STORAGE と WRITE_EXTERNAL_STORAGE の古い権限モデルを置き換え、アプリに分離されたサンドボックス内の自ファイルと、MediaStore を介した共有コレクションへのアクセスのみを許可します。このモデルはユーザーデータのプライバシーを強化し、アプリによる不正な情報収集を防止します。
重要なポイント
Scoped Storage は Android 10(API 29)におけるアーキテクチャ上の変更であり、デバイスのファイルシステムへのアプリのアクセス方法を根本的に変えます。共有ストレージへの完全なアクセスの代わりに、アプリは自身のサンドボックスと、MediaStore を介した特定のメディアコレクションにのみアクセスできます。
Scoped Storage 以前は、READ_EXTERNAL_STORAGE 権限を持つアプリは共有ストレージ内のすべてのファイル(写真、ドキュメント、ダウンロード、他のアプリのデータ)を読み取ることができました。これはプライバシーリスクを生み出していました。信頼できないアプリがユーザーの知らないうちに情報を収集する可能性があったのです。
Google I/O 2019 によると、Android ユーザーの65%以上がアプリ選択時にデータプライバシーを重要な要素と考えています。Scoped Storage はこの需要に直接応えるものです。各アプリは分離された環境で動作し、ユーザーの明示的な同意がある場合にのみ他のアプリのデータにアクセスします。
重要:Android 10 の Scoped Storage はオプションでした(アプリは requestLegacyExternalStorage で無効化できました)。Android 11(API 30)以降、Scoped Storage は target SDK に関係なくすべてのアプリに必須となりました。
Google は、古いファイルアクセスモデルの3つの根本的な問題(プライバシー、権限管理、残存ファイルのクリーンアップ)を解決するために Scoped Storage を導入しました。
古いモデルでは、READ_EXTERNAL_STORAGE を持つアプリが共有ストレージ全体をスキャンし、ユーザーファイルのメタデータ(写真の地理座標、ドキュメント名、ディレクトリ構造)を収集できました。Scoped Storage はこの可能性を排除します。権限があっても、アプリは自身が作成したファイルと、SAF を通じてユーザーが明示的に選択したファイルのみを表示します。
Scoped Storage 以前は、アプリはアンインストール後も共有ストレージにファイルを残すことができました。時間が経つにつれて、ジャンクディレクトリが蓄積されていました。Scoped Storage はこれを解決します。アプリのサンドボックス内のすべてのファイルはアプリとともに削除され、MediaStore 経由のファイルには所有者が存在し、システムがクリーンアップできます。
| 側面 | 古いモデル(Legacy) | Scoped Storage |
|---|---|---|
| 共有ストレージへのアクセス | 完全(権限あり) | 自身のサンドボックスのみ |
| メディアファイル | 直接ファイルパス | MediaStore URI経由 |
| 他のアプリのファイル | 常にアクセス可能 | SAF経由のみ |
| アンインストール時のクリーンアップ | ファイルが残る | サンドボックスが削除される |
| ユーザーの制御 | 最小限 | ファイルごとに明示的な同意 |
Scoped Storage は Android における Google の全体的な プライバシー 戦略の一部であり、「使用中のみ」の権限、カメラとマイクのアクセスインジケーター、プライバシーダッシュボードも含まれます。
各 Android アプリは、Context.getFilesDir() と context.getCacheDir() を介してアクセスできる独自のプライベートディレクトリを取得します。外部(共有)プライベートディレクトリにアクセスするには、Context.getExternalFilesDir() を使用します。
Scoped Storage では、アプリは権限なしで外部プライベートディレクトリに 完全アクセス できます。これは、他のアプリがアクセスできないようにするファイルや、アプリが自身の使用のために作成するファイルを保存するための主要な場所です。
import android.os.Environment
import java.io.File
import java.io.IOException
class FileManager {
fun saveToAppStorage(context: Context, fileName: String, data: ByteArray) {
val appDir = context.getExternalFilesDir(null)
?: return
val file = File(appDir, fileName)
file.writeBytes(data)
}
fun readFromAppStorage(context: Context, fileName: String): ByteArray? {
val appDir = context.getExternalFilesDir(null)
?: return null
val file = File(appDir, fileName)
return file.takeIf { it.exists() }?.readBytes()
}
}
外部プライベートディレクトリへのパス: /storage/emulated/0/Android/data/{packageName}/files/。Android 11 以降、直接ファイルパスは利用できません(API経由のみ)。これは Scoped Storage のもう一つのセキュリティ強化です。
MediaStore は、Scoped Storage において共有メディアファイル(画像、動画、音声)にアクセスするための主要なAPIです。アプリはファイルパスではなく、MediaStore プロバイダーが提供する content URI で動作します。
MediaStore は Images、Video、Audio の3つの主要コレクションに分かれています。各コレクションは ContentResolver を介した CRUD 操作をサポートします。Images コレクションに新しいファイルを挿入するには MediaStore.Images.Media を使用し、既存ファイルをクエリするには対応する URI で query を使用します。
import android.content.ContentValues
import android.provider.MediaStore
import android.os.Environment
import java.io.OutputStream
fun saveImageToGallery(context: Context, bitmap: Bitmap, title: String) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/MyApp"
)
}
val uri = context.contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: return
context.contentResolver.openOutputStream(uri)?.use { output: OutputStream ->
bitmap.compress(
Bitmap.CompressFormat.JPEG,
95,
output
)
}
}
重要:MediaStore は任意のファイルパスをサポートしません — content URI のみです。MediaStore.Files.getContentUri() を介して URI からファイルパスを取得しようとしても、Android 11+ ではファイルへの直接パスは得られません。代わりに、ContentResolver.openInputStream() と openOutputStream() を使用してコンテンツを操作してください。
Storage Access Framework(SAF)は、アプリのサンドボックス外の任意のファイルやディレクトリにアクセスするためのAPIです。SAF はユーザーにファイルまたはディレクトリ選択インターフェースを提供し、アプリは一時アクセス権付きの content URI を取得します。
SAF は MediaStore がカバーしないシナリオで使用されます。任意のドキュメント(PDF、ZIP、APK)の操作、SD カード上のディレクトリへのアクセス、他のアプリからのファイルのインポートとエクスポート。ユーザーはシステムファイルピッカーを介して明示的にファイルを選択します。これによりアクセスへの同意が保証されます。
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
const val REQUEST_CODE_PICK_DIR = 1001
fun pickDirectory(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
activity.startActivityForResult(intent, REQUEST_CODE_PICK_DIR)
}
fun handlePickResult(requestCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_DIR && data != null) {
val treeUri: Uri = data.data ?: return
// Take persistent URI permission
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF は、takePersistableUriPermission を使用すると、デバイス再起動までユーザーが選択したディレクトリへのアクセスをアプリに提供します。これは Android 11+ の共有ストレージで任意のファイルへのアクセスを取得する唯一の方法です。
既存の Android アプリを Scoped Storage に移行するには、いくつかの主要領域での変更が必要です。Google は Android 11+ でのテストを含む段階的アプローチを推奨しています。
共有ストレージ内のすべての直接的な File 操作を ContentResolver.openInputStream / openOutputStream に置き換えます。アプリのサンドボックス(getExternalFilesDir)内のファイルについては、ファイルパスは引き続き機能します。
特定のシナリオ(ファイル管理など — これには特別な権限 MANAGE_EXTERNAL_STORAGE があります)で必要ない場合は、READ_EXTERNAL_STORAGE と WRITE_EXTERNAL_STORAGE を削除します。
| シナリオ | 古いアプローチ | Scoped Storageのアプローチ |
|---|---|---|
| 写真の保存 | File(path).writeBytes() | MediaStore + ContentResolver |
| PDFの読み取り | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| 自ファイル | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| キャッシュ | File(cacheDir).writeBytes() | context.cacheDir(変更なし) |
移行後、build.gradle で targetSdk = 30 以上に設定して Android 11+(API 30) でアプリをテストします。共有ストレージ内のすべてのファイル操作が、直接ファイルパスではなく MediaStore または SAF を介して機能することを確認してください。
import android.os.Build
fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun compatReadFile(context: Context, uri: Uri): ByteArray? {
return if (isScopedStorage()) {
context.contentResolver
.openInputStream(uri)?.readBytes()
} else {
File(uri.path ?: return null)
.takeIf { it.exists() }
?.readBytes()
}
}
Google Play Console(2024) によると、アクティブな Android デバイスの78%以上が Scoped Storage 対応の Android 10+ で動作しています。Google Play でアップデートを公開するには移行が必須です。新しいアプリは API 31+、アップデートは API 30+ をターゲットとする必要があります。
よくある質問
いいえ。Android 11(API 30)以降、Scoped Storage はすべてのアプリに必須です。Android 10 で利用可能だった requestLegacyExternalStorage フラグは API 30+ では機能しません。ファイルシステムへの広範なアクセスを取得する唯一の方法は MANAGE_EXTERNAL_STORAGE 権限ですが、これはファイルマネージャーとウイルス対策アプリ専用です。
MANAGE_EXTERNAL_STORAGE は、完全なファイルシステムアクセスを必要とするアプリ(ファイルマネージャー、バックアップツール)向けの特別な権限です。権限をリクエストすると、ユーザーに警告付きのシステム画面が表示されます。Google Play で公開する際は、この権限の必要性に関する宣言フォームに記入する必要があります。
Android 11+ の Scoped Storage では、content URI から直接ファイルパスを取得できません。代わりに、読み取りには ContentResolver.openInputStream()、書き込みには openOutputStream() を使用します。サードパーティライブラリとの互換性のためにファイルパスが必要な場合は、getCacheDir() にファイルのコピーを作成してコピーを操作します。
FileProvider は変更なしで動作し続けます。アプリのサンドボックスから他のアプリへの content URI を介したファイルアクセスを提供するために使用されます。FileProvider は content URI レベルで動作し、直接ファイルパスではないため、Scoped Storage の影響を受けません。
Build.VERSION.SDK_INT を確認します。>= Build.VERSION_CODES.Q(29)の場合、デバイスは Scoped Storage をサポートしています。ただし、Android 10 では requestLegacyExternalStorage を介して Scoped Storage が無効化されている可能性があります。Android 11+ では、マニフェストフラグ requestLegacyExternalStorage が設定されているかどうかを確認します。設定されていない場合、Scoped Storage が有効です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。