はじめに(対象読者・この記事でわかること)

この記事は、Androidアプリで「ファイルをストレージに保存したいのにアクセス拒否(EACCES)で失敗する」というエラーに悩んでいるJava開発者を対象にしています。
具体的には、Android 10以降でEnvironment.getExternalStorageDirectory()/sdcard/直下への書き込みができなくなった方、あるいは「Permission denied」が出続けて実装が止まっている方向けです。
記事を読み終えると、Scoped Storageの仕組みが理解でき、MANAGE_EXTERNAL_STORAGE権限・Storage Access Framework(SAF)・App-specificディレクトリの3パターンで、いつどの手段を選べば安全かつ確実にファイル書き込みできるかが判断できるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Android StudioでJavaプロジェクトをビルド・実行できる - AndroidManifest.xmlにパーミッションを追記したことがある - 基本的なファイルI/O(FileOutputStream等)の記述経験

Scoped Storage導入で変わったこと

2019年のAndroid 10(API 29)から導入されたScoped Storageは、従来の「ストレージ全体に自由にアクセス」というモデルを廃止し、アプリ自身のディレクトリとメディアコレクション以外の書き込みを原則禁止にしました。
結果として、以下のコードはもはや動作しません。

Java
File legacy = new File(Environment.getExternalStorageDirectory(), "memo.txt"); new FileOutputStream(legacy); // IOException: Permission denied

さらにAndroid 11(API 30)ではrequestLegacyExternalStorage=trueの回避策も封鎖され、ストレージ権限の獲得がより厳格になりました。
Play Store掲載を考える場合、Googleは「すべてのファイルアクセス」を許可するMANAGE_EXTERNAL_STORAGE権限の審査を非常に厳しくしており、安易に使用するとリジェクト対象になります。
そのため、まず「何のためにファイルを書き込むのか」を明確にし、要件に応じて以下3つの手段から選ぶ必要があります。

  1. App-specific(内部/外部)ディレクトリへ保存(他アプリから参照は不可)
  2. メディアコレクション(画像・動画・音楽)へ書き込み(SAF経由)
  3. 任意の場所へ自由に書き込み(MANAGE_EXTERNAL_STORAGE、審査必須)

ファイル書き込みを成功させる3つの実装パターン

ステップ1:App-specific外部ディレクトリを使う(推奨)

最も手軽でPlay Storeにも対応する方法です。キャッシュ扱いではないため、ユーザーがアンインストールするまでデータは保持されます。

Java
// 1. ディレクトリ取得 File docsDir = getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS); File outFile = new File(docsDir, "memo.txt"); // 2. 書き込み(try-with-resources) try (FileOutputStream fos = new FileOutputStream(outFile); OutputStreamWriter writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { writer.write("Hello Scoped Storage!"); }

AndroidManifest.xmlにパーミッションは不要です。
他アプリから読み込ませたい場合はFileProviderでURIを共有してください。

ステップ2:SAF(Storage Access Framework)でユーザーに選ばせる

ドキュメントやダウンロードフォルダなど、任意の場所に保存したいときはSAFを使います。
ユーザーが1回だけディレクトリ/ファイルを選ぶことで、以降はtakePersistableUriPermission()で継続アクセスできます。

Java
// 1. インテント発行 Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TITLE, "memo.txt"); startActivityForResult(intent, REQ_CREATE); // 2. onActivityResult @Override protected void onActivityResult(int req, int res, Intent data) { if (req == REQ_CREATE && res == RESULT_OK && data != null) { Uri uri = data.getData(); getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION); try (OutputStream out = getContentResolver().openOutputStream(uri)) { out.write("Hello SAF".getBytes(StandardCharsets.UTF_8)); } } }

SAFを使えばREAD_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGEも不要です。

ステップ3:MANAGE_EXTERNAL_STORAGEを使う(最終手段)

アプリのコア機能が「ファイルマネージャ」「バックアップツール」「ウイルススキャン」など、どうしてもすべてのファイルアクセスが必要な場合は、以下を行います。

  1. AndroidManifest.xmlに宣言
Xml
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
  1. ユーザーに許可を求める
Java
if (!Environment.isExternalStorageManager()) { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); startActivity(intent); }
  1. Play Console提出時に「アプリコンテンツ」→「すべてのファイルアクセス権限」で用途を記述し、動画・スクリーンショットを添付して審査を受ける

審査ポイント
・「ファイルマネージャ」以外のカテゴリだとほぼリジェクト
・ユーザーが任意に選択/削除できるUIを提示しているか
・プライバシーポリシーに明記

ハマった点・エラー解決

エラー1:open failed: EACCES (Permission denied)が消えない

原因:Android 10でrequestLegacyExternalStorage=trueを入れ忘れた、もしくはAndroid 11以降でそのフラグが無効化されている
対処:ステップ1またはステップ2に移行する

エラー2:FileProviderIllegalArgumentException: Couldn't find meta-data

原因:パススキームが正しくない
対処:xml/path.xmlにexternal-files-pathを正しく定義し、authorities属性が一致しているか再確認

エラー3:SAFでtakePersistableUriPermissionが失敗

原因:URIがcontent://でない、もしくは既に許可済み
対処:既存のUriPermissionをgetContentResolver().getPersistedUriPermissions()で確認し、重複取得を避ける

解決策まとめ

  • まず「どこに書き込む必要があるか」を整理する
  • 他アプリ参照不要 → App-specific外部ディレクトリ(ステップ1)
  • ユーザーが自由に管理したい → SAF(ステップ2)
  • ツール系アプリで避けられない → MANAGE_EXTERNAL_STORAGE(ステップ3)+Play審査

まとめ

本記事では、Android 10以降でJavaの従来ファイルAPIが使えなくなった背景と、Scoped Storage下で安全に書き込む3つの方法を解説しました。

  • App-specificディレクトリならパーミッション不要で即書き込み可能
  • SAFを使えばユーザーに場所を選ばせつつ、永続URIで再アクセス
  • MANAGE_EXTERNAL_STORAGEは審査が厳しいため、ツール系アプリ以外は避ける

この記事を通して、読者は「アクセス拒否」で詰まることなく、Play Store対応も見据えたファイル保存実装ができるようになります。
次回は、Jetpack ComposeとSAFを組み合わせた「ドキュメント選択UI」のカスタマイズ方法を取り上げます。

参考資料