メインコンテンツへスキップ

CapacitorアプリでのAndroid Scoped Storage

Capacitor アプリにおけるAndroid scoped storageの解説:Android 10から14での変更点、引き続き機能する権限、MediaStore、SAF、安全なディレクトリについて。

Capacitor アプリのファイルに適用される Android スコープ付きストレージのルール

AndroidのScoped Storageは、Capacitorアプリが自身のフォルダ外で読み書きできる内容を制限します。Android 10から導入され、Android 11以降で完全に適用されたこの仕組みでは、アプリは自身のプライベートディレクトリを自由に利用でき、MediaStoreを通じて共有コレクションにファイルを追加できます。それ以外の操作では、Storage Access Frameworkを介してユーザーが選択する必要があります。本ガイドでは、Androidのバージョンごとの変更点や、どのCapacitor Directory の値が引き続き機能するのか、Capacitor 8でサポートされているファイルの保存・インポート・エクスポート方法について解説します。

バージョンごとの変更点

Androidでは API Capacitor アプリにおける重要な変更
10 29 Scoped storage が導入されました。以下でオプトアウトできます: requestLegacyExternalStorage="true" API 29 をターゲットにする場合
11 30 30 以降をターゲットとするアプリでは、Scoped storage が強制適用されます。 requestLegacyExternalStorage 無視されます。 WRITE_EXTERNAL_STORAGE 何も付与されなくなりました。 MANAGE_EXTERNAL_STORAGE 導入されました。Tree picker では Download ルートやボリュームルートを選択できなくなりました
12 31-32 ストレージに関する大きな変更なし
13 33 READ_EXTERNAL_STORAGE に置き換えられました READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO. 他のアプリのドキュメントにアクセスできる権限はありません
14 34 による写真への部分的なアクセス READ_MEDIA_VISUAL_USER_SELECTED
15 および 16 35-36 新しいストレージモデルはなく、写真と動画の権限に関する Play ポリシーが適用されます

Capacitor は API 36 を対象としているため、最新のデバイスでは常にフルスコープストレージで動作します。以前の挙動に戻すフラグは存在しません。

古い Capacitor code で何が壊れたのか

Capacitor 2 または 3 用に書かれた Code や、Cordova から移植されたものは、新しいデバイスでサイレントに失敗することがよくあります:

  • Filesystem.writeFile({ directory: Directory.Documents }) または Directory.ExternalStorage 「端末に保存」について。Android 11以降、これらのパスは共有ストレージを指すため、書き込みに失敗したり、再インストール後にファイルが読み込めなくなったりします。
  • 次のような任意のパスの読み取り: /storage/emulated/0/Download/file.pdf 設定で入力されたパスや、サーバーから受信したパスから。
  • リクエスト READ_EXTERNAL_STORAGE ユーザーのドキュメントを列挙するために使用します。Android 13以降では、メディアファイル以外に対してこの権限は何も機能しません。
  • 使用する Filesystem.requestPermissions() そして、 granted アプリがどこにでも書き込めることを意味します。
  • ピッカーが返す URI から content:// ファイルパスを構築する。

権限なしでも動作する機能

アプリ自身のディレクトリはスコープ付きストレージの影響を受けず、権限も不要です:

Capacitor Directory Android のパス ユーザーに表示 アンインストール時に削除
Data, Library, LibraryNoCloud /data/data/<package>/files いいえ はい
Cache /data/data/<package>/cache いいえ はい
External /storage/emulated/0/Android/data/<package>/files USBまたは一部のファイルマネージャー経由のみ はい
ExternalCache /storage/emulated/0/Android/data/<package>/cache 同じ はい
Documents 共有 Documents フォルダ はい いいえ
ExternalStorage 共有ストレージのルート はい いいえ

最初の4行は安全です。 Documents と ExternalStorage はレガシーな行です。Android 11 以降、アプリはそこに存在するファイルのうち、自分が作成したもののみ参照できます。再インストール後は、それらへのアクセス権も失われます。新しい code では非サポートとして扱うべきです。

External は、大きなファイル(オフライン動画、マップなど)に有用です。より大きなパーティション上に存在する可能性があり、そのファイルは Auto Backup の対象外です。Android 11 以降、他のアプリは Android/dataを読み取ることができないため、実質的には依然としてプライベートです。

ユーザーが見つけられる場所にファイルを保存: MediaStore

ユーザーが請求書で「ダウンロード」をタップすると、Downloads フォルダに保存されることを期待します。Android 10 以降、アプリは権限なしで共有 Downloads, Pictures, Movies, Music と Documents コレクションにファイルを挿入できます(MediaStore を使用)。

@capgo/capacitor-file-sharer これをラップします save():

bun add @capgo/capacitor-file-sharer
bunx cap sync
import { FileSharer } from '@capgo/capacitor-file-sharer';
import { Filesystem, Directory } from '@capacitor/filesystem';

const { uri } = await Filesystem.getUri({ path: 'exports/invoice-1042.pdf', directory: Directory.Data });

const result = await FileSharer.save({
  filename: 'invoice-1042.pdf',
  path: uri,
  contentType: 'application/pdf',
  android: {
    saveDirectory: 'downloads',     // 'downloads' | 'pictures' | 'movies' | 'music' | 'documents'
    relativePath: 'MyApp/Invoices', // subfolder, Android 10+
  },
});

console.log(result.uri); // content:// URI of the saved file

iOSでは同じ呼び出しでシェアシートが開き、ユーザーが「ファイルに保存」を選ぶので、1つの code パスで両プラットフォームに対応できます。

Android 9以下で共有ストレージに書き込むには、引き続き WRITE_EXTERNAL_STORAGE。それらのバージョンでのみ宣言してください:

<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />

Capacitor 8はAndroid 7 (API 24)以降をサポートしているため、Android 7〜9のデバイスにまだ配信しているなら、その行は残してください。

写真と動画

メディアについては、自分でファイルを書き込むよりプラットフォームのフローを優先してください。 @capacitor/camera を使用してギャラリーに書き込みます saveToGallery: true、古いAndroidバージョンでのみレガシーストレージ権限が必要です。詳細はこちらのガイドをご覧ください: Capacitor での写真の撮影と編集について.

ユーザーファイルの読み取り:権限の代わりにピッカーを使う

Scoped storageでは、ユーザーがファイルを選択することを前提としています。Androidのピッカーは content:// ストレージ権限がなくてもアプリが読み取れるURI。

import { CapgoFilePicker } from '@capgo/capacitor-file-picker';
import { Filesystem, Directory } from '@capacitor/filesystem';

const { files } = await CapgoFilePicker.pickFiles({ types: ['application/pdf'], limit: 1 });
const picked = files[0];

// Copy into app storage right away: picker grants can expire
await Filesystem.copy({
  from: picked.path!,
  to: `imports/${picked.name}`,
  toDirectory: Directory.Data,
});

写真や動画の場合は、 @capgo/capacitor-file-pickerの pickMedia と @capacitor/cameraの chooseFromGallery Android 13以降では、権限が一切不要なAndroid Photo Pickerを使用してください。

READ_MEDIA_* 権限を避ける

Google Playの写真・動画権限ポリシーでは、 READ_MEDIA_IMAGES と READ_MEDIA_VIDEO ギャラリーアプリや写真編集アプリなど、コア機能として幅広いギャラリーアクセスが必要なアプリにのみ許可されています。それ以外のアプリはPhoto Pickerを使用し、Play Consoleでその旨を宣言する必要があります。使用しているプラグインがこれらの権限を宣言している場合は、マージされたマニフェストから削除してください:

<uses-permission
    android:name="android.permission.READ_MEDIA_IMAGES"
    tools:node="remove" />
<uses-permission
    android:name="android.permission.READ_MEDIA_VIDEO"
    tools:node="remove" />

追加 xmlns:tools="http://schemas.android.com/tools" への <manifest> タグを先に設定します。

Storage Access Frameworkによるフォルダアクセス

フォルダへの継続的なアクセスが必要なアプリがあります。ユーザーフォルダに同期するメモアプリや、フィールドアプリのエクスポートフォルダなどです。Storage Access Framework (SAF) を使用すると、ユーザーがフォルダを ACTION_OPEN_DOCUMENT_TREE:

const { path } = await CapgoFilePicker.pickDirectory();
// On Android: a content://com.android.externalstorage.documents/tree/... URI

3つのポイント:

  1. 結果はパスではなく、ツリー URI です。 これを渡すことはできません Filesystem.writeFile。内部のファイルは、以下を通じて作成・読み取ります DocumentsContract または DocumentFile ネイティブ code 内で。
  2. アクセス権限は再起動後も維持されません アプリが URI に対して takePersistableUriPermission を呼び出さない限り。必要であれば、小さなネイティブメソッドを追加してください:
@PluginMethod
public void persist(PluginCall call) {
    Uri uri = Uri.parse(call.getString("uri"));
    getContext().getContentResolver().takePersistableUriPermission(
        uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    call.resolve();
}
  1. 権限を付与できないフォルダがあります。 Android 11以降では、内部ストレージのルートや、 Download フォルダのルート、および Android/data と Android/obbは、ピッカーで選択できません。ユーザーにはサブフォルダを作成するか選択するよう案内してください。

永続的な権限はアプリごとに数が制限されており、ユーザーはシステム設定やアプリデータの消去によって取り消すことができます。アクセスは以下で確認します。 getPersistedUriPermissions() 起動時に確認し、失われていれば再度リクエストしてください。

全ファイルへのアクセス: MANAGE_EXTERNAL_STORAGE

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

この権限は従来の「すべてを読み書きできる」動作を実現しますが、ランタイムプロンプトではありません。ユーザーが専用の設定画面で有効にする必要があり、Google Play による手動審査も行われます。許可されるユースケースは、ファイルマネージャー、バックアップと復元、アンチウイルス、ドキュメント管理、端末内のファイル検索など、広範なアクセスがコア機能となるものに限られます。メモアプリや、ダウンロードフォルダからファイルを開くPDFビューア、「エクスポートを保存するだけ」のアプリは却下されます。

Play 以外(企業のMDMやサイドロードされたキオスク端末など)で配布する場合は選択肢に入ります。それ以外は MediaStore と SAF を前提に設計してください。

他のプラグインとの関係

プラグイン Scoped storage の影響
@capacitor/filesystem アプリディレクトリ内であれば問題ありません。 Documents と ExternalStorage Android 11 以降ではレガシーです。 requestPermissions() 旧Androidのディレクトリでのみ重要
@capacitor/camera システムカメラとPhoto Pickerを使用。ストレージ権限は不要 saveToGallery Android 9以前の場合
@capacitor/share を介して共有 FileProvider。にリストされたパスのみ res/xml/file_paths.xml 共有可能
@capgo/capacitor-file-sharer 独自の FileProvider とMediaStoreを使用 save()、設定不要
@capgo/capacitor-downloader アプリ外部ストレージにダウンロードし、 save() でエクスポート
@capgo/capacitor-file-picker Pickerベース。Android 13以降は権限不要
@capgo/capacitor-photo-library そのマニフェストに追加します READ_MEDIA_* 上記のPlayポリシーに従い、ライブラリ全体を読み取ります。 pickMedia() システムピッカーを使用し、権限は不要です。これが唯一の用途であれば、 READ_MEDIA_* を使用して tools:node="remove"

どのディレクトリを使うか

  • アプリデータ、設定、データベース: Directory.Data
  • 一時ファイルと共有エクスポート: Directory.Cache
  • 再ダウンロード可能な大容量メディア: Directory.External Androidでは、 Directory.LibraryNoCloud iOSの場合
  • ユーザーがDownloadsやPicturesに置きたいファイル:MediaStore経由で FileSharer.save()
  • ユーザーがアプリに取り込むファイル:pickerで選択してコピー Directory.Data
  • アプリに継続的に書き込ませたいフォルダ:永続化された許可付きのSAF tree URI

読み書きの完全な API については、 Capacitor ファイルハンドリングガイド.

既存アプリの移行

  1. code 内で検索 Directory.Documents, Directory.ExternalStorage, /storage/emulatedや requestLegacyExternalStorage.
  2. アプリ所有のファイルを以下へ移動 Directory.Data または Directory.External. 新バージョンの初回起動時に、まだ読める古いファイルをすべてコピーしてから削除してください。
  3. 「デバイスへの保存」機能を FileSharer.save().
  4. パス入力はピッカーに置き換えてください。
  5. 追加 android:maxSdkVersion レガシーなストレージ権限に置き換え、 READ_MEDIA_* 必要でない限り削除してください。
  6. Android 10、11、13、15 または 16 の実機またはエミュレータでテストしてください。挙動はまさにこれらの境界で異なります。

preserveLegacyExternalStorage="true" の <application> タグは、以前のバージョンからアップデートされたアプリのレガシーアクセスを維持します requestLegacyExternalStorage, アプリが再インストールされるまでです。一度限りの移行ステップでのみ役立つので、これに依存しないでください。

トラブルシューティング

EACCES (Permission denied) への書き込み /storage/emulated/0/...:共有ストレージに直接書き込んでいます。MediaStore またはアプリディレクトリを使用してください。

ファイルが保存されました Directory.Documents 再インストール後に消えます:アプリが所有権を失いました。ユーザーファイルには MediaStore を使用してください。

FileNotFoundException 後で content:// URI を読み取る:一時的な許可の有効期限が切れました。ファイルは選択時にコピーするか、ツリー URI の許可を永続化してください。

Play Console の写真・動画権限に関する警告:プラグインが追加した READ_MEDIA_IMAGES または READ_MEDIA_VIDEO。不要な場合は tools:node="remove" で削除してください。

requestPermissions() granted を返すが、書き込みは依然として失敗する:Android 11 以降では、レガシーストレージ権限は付与されても共有ストレージでは意味がありません。

ネイティブとWeb部分の同期を維持

ストレージのルールはネイティブ設定なので、マニフェストやプラグインの変更にはストアリリースが必要で、 Capgo のビルド クラウドで生成できます。それらを取り巻くファイル処理(パス、フォルダ名、エクスポートフロー)は、JavaScriptで記述されており、 Capgo Live Updates.

Capacitor アプリ向けの Live Updates

Web レイヤーのバグが本番で発生したら、App Store の審査を何日も待たずに Capgo で修正を配信しましょう。ネイティブの変更は通常の審査フローに残したまま、ユーザーはバックグラウンドでアップデートを受け取れます。

最新のブログ記事

Capgoは、真にプロフェッショナルなモバイルアプリを作るために必要な最高のインサイトを提供します。