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

この記事は、Androidアプリ開発において、Java言語を使用して外部ストレージ(SDカードなど)にフォルダを作成しようとした際に、「フォルダが作成できない」という問題に直面している開発者を対象としています。特に、Android 4.4 (KitKat) 以降で導入されたScoped Storageの概念や、それ以前のストレージアクセス権限の変更に戸惑っている方におすすめです。

この記事を読むことで、Androidの外部ストレージへのフォルダ作成ができない主な原因を理解し、APIレベルに応じた適切なアプローチでフォルダ作成を成功させるための具体的なコード例と、開発中に遭遇しがちなエラーとその解決策について学ぶことができます。これにより、ストレージ操作に関する開発の効率が向上し、より安定したアプリ開発に繋がることを目指します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * Java言語の基本的な文法 * Android開発の基本的な流れ(Activity, Manifestファイルなどの理解) * ストレージへのファイルアクセスに関する基本的な概念(内部ストレージと外部ストレージの違いなど)

Android 11以降で外部ストレージにフォルダが作成できない?Scoped Storageの壁

Android 11 (APIレベル 30) からは、Scoped Storageという厳格なストレージアクセスモデルがデフォルトで有効になりました。これにより、アプリは自身のアプリ専用ディレクトリ(Context.getExternalFilesDir()など)へのアクセスは比較的容易ですが、共有ストレージ(Downloads, Picturesなど)への直接的なフォルダ作成やファイル書き込みには、より限定的な方法しか許可されなくなっています。

以前は、WRITE_EXTERNAL_STORAGE パーミッションをマニフェストに記述し、実行時にユーザーに許可を求めることで、外部ストレージの任意の場所にアクセスできました。しかし、Scoped Storageの導入により、このアプローチは推奨されなくなり、特定のユースケースでは機能しなくなっています。

具体的には、アプリが生成したファイルを他のアプリからアクセス可能にする場合や、ユーザーが手動で管理するべきファイル(例: 写真、ドキュメント)を保存する場合には、MediaStore APIやStorage Access Framework (SAF) を利用することが強く推奨されます。File クラスを使った直接的なパス指定によるフォルダ作成は、Scoped Storageが有効な環境では、アプリ専用ディレクトリ以外では原則として制限されます。

したがって、外部ストレージにフォルダを作成する際には、まず「なぜフォルダを作成したいのか」「作成したフォルダやファイルに、他のアプリからもアクセスできるようにしたいのか」といった目的を明確にし、それに適したAPIを選択することが重要です。

外部ストレージへのフォルダ作成:APIレベル別アプローチと実践コード

Androidのバージョンによって、外部ストレージへのフォルダ作成方法が大きく異なります。ここでは、APIレベルごとの主要なアプローチと、具体的なJavaコード例を解説します。

1. Android 10 (APIレベル 29) 以前:WRITE_EXTERNAL_STORAGE パーミッションと File クラス

Android 10以前では、WRITE_EXTERNAL_STORAGE パーミッションを適切に宣言し、ユーザーの許可を得ることで、File クラスを利用して外部ストレージの任意の場所にフォルダを作成できました。

AndroidManifest.xml への追記:

Xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.yourapp"> <!-- Android 9 (API 28) 以前のストレージアクセス --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- Android 10 (API 29) では、Scoped Storageの恩恵を受けるために、 TARGET_SDK_VERSION を 29 に設定し、File API でのアクセスを アプリ固有のディレクトリに限定するか、MediaStore API を利用することが推奨されます。 この permission は、API 29 では不要になる、もしくは制限されます。 --> <application ...> ... </application> </manifest>

Java コード例 (フォルダ作成):

Java
import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; import android.os.Environment; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.appcompat.app.AppCompatActivity; import android.widget.Toast; import java.io.File; public class MainActivity extends AppCompatActivity { private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 1; // ... Activity の onCreate など ... private void createFolderInExternalStorage() { // 外部ストレージのルートディレクトリを取得 File root = Environment.getExternalStorageDirectory(); // 作成したいフォルダ名 String folderName = "MyCustomFolder"; File folder = new File(root, folderName); if (!folder.exists()) { // フォルダが存在しない場合、作成を試みる if (folder.mkdirs()) { // フォルダ作成成功 Toast.makeText(this, folderName + " フォルダを作成しました。", Toast.LENGTH_SHORT).show(); } else { // フォルダ作成失敗 Toast.makeText(this, folderName + " フォルダの作成に失敗しました。", Toast.LENGTH_SHORT).show(); } } else { // フォルダは既に存在 Toast.makeText(this, folderName + " フォルダは既に存在します。", Toast.LENGTH_SHORT).show(); } } // パーミッションリクエストの結果を処理 @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // パーミッションが付与されたので、フォルダ作成処理を実行 createFolderInExternalStorage(); } else { // パーミッションが付与されなかった Toast.makeText(this, "外部ストレージへの書き込み権限が必要です。", Toast.LENGTH_SHORT).show(); } } } // フォルダ作成を試みる前にパーミッションをチェック・リクエスト private void checkAndRequestPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Android 6.0 (API 23) 以降は実行時パーミッション if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // パーミッションが未許可の場合、リクエストする ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_EXTERNAL_STORAGE); } else { // パーミッションが既に付与されている場合 createFolderInExternalStorage(); } } else { // Android 6.0 未満はマニフェストで許可されていればOK createFolderInExternalStorage(); } } // ... onCreateなどで checkAndRequestPermission() を呼び出す ... }

注意点: * Environment.getExternalStorageDirectory() は、API 29 (Android 10) からはScoped Storageの影響を受け、アプリ固有のディレクトリ以外へのアクセスには注意が必要です。API 30 (Android 11) 以降では、このメソッドは非推奨になり、getExternalFilesDir() などの、より限定されたAPIの使用が推奨されます。 * mkdirs() は、指定したパスのディレクトリを作成します。途中のディレクトリが存在しない場合、それらもまとめて作成します。

2. Android 10 (APIレベル 29) から Android 10 (APIレベル 29) までの移行期:Scoped Storage と MediaStore API の検討

Android 10 (API 29) では、requestLegacyExternalStorage というマニフェスト属性を設定することで、Scoped Storage の影響を一時的に緩和することができました。しかし、これはあくまで移行期間中の措置であり、Android 11 以降では無効になります。

この期間で、アプリ固有の外部ストレージディレクトリ(getExternalFilesDir())へのフォルダ作成は、File クラスでこれまで通り行うことができます。

アプリ固有の外部ディレクトリへのフォルダ作成:

Java
import android.content.Context; import java.io.File; public class StorageHelper { public static File createFolderInAppSpecificExternalStorage(Context context, String folderName) { // アプリ固有の外部ストレージディレクトリを取得 // (SDカードがマウントされている場合はそちらに、そうでなければ内部ストレージのアプリ専用領域に作成されます) File appSpecificDir = context.getExternalFilesDir(null); // null を渡すとルートディレクトリ File folder = new File(appSpecificDir, folderName); if (!folder.exists()) { if (folder.mkdirs()) { // フォルダ作成成功 } else { // フォルダ作成失敗 } } return folder; } }

しかし、共有ストレージ(Downloads, Picturesなど)へのフォルダ作成やファイル保存をしたい場合は、MediaStore API の利用を検討する必要があります。

3. Android 11 (APIレベル 30) 以降:Scoped Storage の本格適用と MediaStore API / Storage Access Framework (SAF)

Android 11以降では、Scoped Storageがデフォルトで有効になり、File クラスを使った共有ストレージへの直接的な書き込みは大幅に制限されます。共有ストレージにフォルダを作成し、そこにファイルを保存したい場合は、MediaStore API を使用するのが標準的な方法です。

3.1. MediaStore API を使用したフォルダ作成とファイル保存

MediaStore API を使用すると、OSが管理するメディアストアにコンテンツを登録し、適切な権限でアクセスできるようになります。

Java コード例 (Downloadsフォルダにフォルダとファイルを作成):

Java
import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.util.Log; import android.widget.Toast; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; public class MediaStoreHelper { private static final String TAG = "MediaStoreHelper"; public static void createFolderAndFileInDownloads(Context context, String folderName, String fileName, String fileContent) { ContentResolver resolver = context.getContentResolver(); ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain"); // ファイルのMIMEタイプ // ターゲットディレクトリを指定 (Downloadsフォルダ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + folderName); values.put(MediaStore.MediaColumns.IS_PENDING, 1); // 書き込み中は保留状態にする } else { // Android 10 (API 29) 以前では、File API または、MediaStore API のより低レベルな操作が必要になる場合があります。 // この例では、API 29 以降を主眼に置いています。 Toast.makeText(context, "この機能は Android 10 (API 29) 以降で最適に動作します。", Toast.LENGTH_LONG).show(); return; } Uri collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); Uri itemUri = resolver.insert(collection, values); if (itemUri == null) { Log.e(TAG, "Failed to create new MediaStore record."); Toast.makeText(context, "ファイル作成に失敗しました。", Toast.LENGTH_SHORT).show(); return; } try (OutputStream fos = resolver.openOutputStream(itemUri)) { if (fos == null) { Log.e(TAG, "Failed to get output stream for MediaStore record."); // 作成されたレコードを削除するなどの後処理が必要になる場合も resolver.delete(itemUri, null, null); Toast.makeText(context, "ファイル作成に失敗しました。", Toast.LENGTH_SHORT).show(); return; } fos.write(fileContent.getBytes(StandardCharsets.UTF_8)); // 書き込み完了後、保留状態を解除 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.clear(); values.put(MediaStore.MediaColumns.IS_PENDING, 0); resolver.update(itemUri, values, null, null); } Toast.makeText(context, "「" + folderName + "/" + fileName + "」を作成しました。", Toast.LENGTH_LONG).show(); } catch (IOException e) { Log.e(TAG, "Error writing to MediaStore: " + e.getMessage()); // エラー発生時は、作成されたレコードを削除 resolver.delete(itemUri, null, null); Toast.makeText(context, "ファイル作成中にエラーが発生しました。", Toast.LENGTH_SHORT).show(); e.printStackTrace(); } } }

使い方:

Java
// Activity や Fragment 内で MediaStoreHelper.createFolderAndFileInDownloads( this, // Context "MyAppDataFiles", // 作成したいフォルダ名 (Downloadsフォルダ内に作成) "config.txt", // 作成したいファイル名 "Setting1=value1\nSetting2=value2" // ファイルに書き込む内容 );

MediaStore API のポイント: * MediaStore.MediaColumns.RELATIVE_PATH: API 29 以降で、ターゲットディレクトリを指定するために使用します。Environment.DIRECTORY_DOWNLOADS + "/" + folderName のように、Environment クラスで定義された公開ディレクトリ名と、その中に作成したいフォルダ名をスラッシュで区切って指定します。 * MediaStore.MediaColumns.IS_PENDING: 書き込み中は 1 に設定し、書き込み完了後に 0 に更新することで、OSにコンテンツの準備ができたことを通知します。 * ContentResolver.insert(): 新しいレコードを作成し、その Uri を返します。 * ContentResolver.openOutputStream(): 作成した Uri に対応する OutputStream を取得し、ファイルに書き込みます。 * エラー発生時には、resolver.delete(itemUri, null, null) を呼び出して、作成途中のレコードを削除することが重要です。

3.2. Storage Access Framework (SAF) の利用

ユーザーが直接ファイルを選択・保存するような場面では、SAFが適しています。Intent.ACTION_OPEN_DOCUMENT_TREE を使用して、ユーザーにフォルダを選択させ、そのフォルダへの書き込み権限を取得します。

SAF を使ったフォルダ選択とファイル作成の概要:

  1. フォルダ選択 Intent の起動: java Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE);

  2. 結果の処理: onActivityResult で、ユーザーが選択したフォルダの Uri を受け取ります。 java @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE && resultCode == RESULT_OK && data != null) { Uri directoryUri = data.getData(); if (directoryUri != null) { // 選択されたフォルダURIを使って、その中にファイルを作成する createFileInSelectedFolder(directoryUri, "my_document.txt", "Content..."); } } }

  3. 選択されたフォルダ内へのファイル作成: ```java private void createFileInSelectedFolder(Uri parentUri, String fileName, String content) { ContentResolver resolver = getContentResolver(); try { // 選択されたフォルダURIの下に新しいファイルを作成するためのUriを生成 Uri fileUri = resolver.insert( MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), // 汎用的なFilesコレクション new ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); put(MediaStore.MediaColumns.MIME_TYPE, "text/plain"); // parentUri を使って、その直下に作成するように指示 // API 30 以降では、UriMatcher などでより詳細なパス指定が必要になる場合も // この例では、parentUri の直下に新規ファイルを作成する簡略版としています。 // より複雑なパス構造の場合は、resolver.createDocument() などの利用も検討。 } );

        if (fileUri != null) {
            try (OutputStream os = resolver.openOutputStream(fileUri)) {
                if (os != null) {
                    os.write(content.getBytes(StandardCharsets.UTF_8));
                    Toast.makeText(this, "ファイル '" + fileName + "' を作成しました。", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(this, "ファイル作成に失敗しました (OutputStream取得不可)。", Toast.LENGTH_SHORT).show();
                }
            } catch (IOException e) {
                Log.e(TAG, "Error writing file: " + e.getMessage());
                // エラー時は作成されたUriを削除
                resolver.delete(fileUri, null, null);
                Toast.makeText(this, "ファイル作成中にエラーが発生しました。", Toast.LENGTH_SHORT).show();
                e.printStackTrace();
            }
        } else {
            Toast.makeText(this, "ファイル作成に失敗しました (Uri取得不可)。", Toast.LENGTH_SHORT).show();
        }
    } catch (Exception e) {
        Log.e(TAG, "Error creating file: " + e.getMessage());
        Toast.makeText(this, "ファイル作成中に予期せぬエラーが発生しました。", Toast.LENGTH_SHORT).show();
        e.printStackTrace();
    }
    

    } ```

SAF のポイント: * ユーザーが明示的にフォルダを選択するため、プライバシーに配慮した方法です。 * 一度選択されたフォルダは、アプリが再起動しても権限が維持される場合があります(永続的権限)。 * ACTION_OPEN_DOCUMENT_TREE は、ユーザーにフォルダを選ばせるためのインテントです。 * getContentResolver().insert() を使用して、選択された Uri を基点とした新しいファイルを作成します。

ハマった点やエラー解決

  • java.io.FileNotFoundException: open failed: EACCES (Permission denied):

    • 原因: Android 10 (API 29) 以降で、Scoped Storageが有効な状態で File クラスを使って共有ストレージに直接書き込もうとした場合に発生します。WRITE_EXTERNAL_STORAGE パーミッションだけでは不十分です。
    • 解決策: APIレベルに応じて、MediaStore API または Storage Access Framework (SAF) を使用してください。アプリ固有の外部ディレクトリに保存する場合は、Context.getExternalFilesDir() を使用し、パーミッションは不要です。
  • mkdirs()false を返す:

    • 原因:
      • 指定したパスへの書き込み権限がない(Scoped Storageによる制限、またはパーミッション不足)。
      • SDカードがマウントされていない、または読み取り専用になっている。
      • ファイルパスが不正。
      • (非常に稀ですが)ストレージに十分な空き容量がない。
    • 解決策: ログを確認し、APIレベルに合ったストレージアクセス方法を選択しているか確認します。パーミッション関連のエラーであれば、MediaStore API や SAF への移行を検討します。File クラスを使う場合でも、getExternalFilesDir() などのアプリ専用ディレクトリであれば、権限の問題は発生しにくいです。
  • 作成したファイルやフォルダが見つからない:

    • 原因:
      • MediaStore API で IS_PENDING フラグを 0 に更新し忘れている。
      • SAFで、ユーザーが選択したフォルダとは別の場所に保存しようとしている。
      • ファイルマネージャーアプリによっては、MediaStore API で作成されたコンテンツの表示にタイムラグがある場合がある。
    • 解決策: MediaStore API を使用している場合は、書き込み完了後に必ず IS_PENDING0 に更新します。SAF を使用している場合は、parentUri を正しく指定しているか確認します。
  • TARGET_SDK_VERSIONrequestLegacyExternalStorage の混同:

    • 原因: Android 10 (API 29) で requestLegacyExternalStorage="true" を設定することで、一時的に旧来のストレージアクセスに戻せますが、Android 11 (API 30) 以降ではこの属性は無視されます。
    • 解決策: Android 11以降をターゲットにする場合は、Scoped Storage に対応した MediaStore API や SAF の利用を前提として開発を進めます。

まとめ

本記事では、Androidアプリ開発で外部ストレージにフォルダを作成する際に直面する「フォルダが作成できない」という問題について、APIレベルごとの原因と解決策を詳細に解説しました。

  • Android 10 (API 29) 以前: WRITE_EXTERNAL_STORAGE パーミッションと File クラスが主流でしたが、API 29 以降では制限がかかります。
  • Android 11 (API 30) 以降: Scoped Storage がデフォルトで有効となり、共有ストレージへのアクセスは MediaStore API または Storage Access Framework (SAF) を使用することが必須となりました。
  • アプリ固有の外部ディレクトリ: Context.getExternalFilesDir() を使用すれば、パーミッション不要で、APIレベルによらずフォルダ作成が可能です。

この記事を通して、Androidのストレージアクセスに関する最新の仕様を理解し、APIレベルに応じた適切なフォルダ作成・ファイル操作方法を実践できるようになるはずです。今後は、より高度なファイル操作(ファイルの削除、更新、複数ファイルの一括処理など)や、異なるストレージへのアクセス方法についても掘り下げていく予定です。

参考資料