Androidアプリケーション開発において、カメラで撮影した画像をデバイスのギャラリーに保存する機能は一般的です。しかし、特定の条件下でjava.lang.IllegalStateExceptionが発生し、画像の保存に失敗するケースが報告されています。本記事では、この問題の原因と、MediaStoreへのファイル保存時にユニークなファイル名を確保するための対策について解説します。
問題の発生
アプリケーションが画像を撮影し、そのデータをシステムギャラリーに登録するためにMediaStore.Images.Media.insertImage()メソッドを利用していた際に、突然画像保存が失敗するようになりました。ログには以下のようなIllegalStateExceptionが記録されていました。
E DatabaseUtils: Writing exception to parcel
E DatabaseUtils: java.lang.IllegalStateException: Failed to build unique file: /storage/emulated/0/Pictures/Image.jpg bucket_display_name=Pictures volume_name=external_primary date_modified=null date_expires=null _display_name=Image.jpg mime_type=image/jpeg _data=/storage/emulated/0/Pictures/Image.jpg _size=null is_trashed=0 is_pending=0 bucket_id=-1617409521 relative_path=Pictures/
E DatabaseUtils: at com.android.providers.media.MediaProvider.ensureFileColumns(MediaProvider.java:2624)
E DatabaseUtils: at com.android.providers.media.MediaProvider.ensureUniqueFileColumns(MediaProvider.java:2368)
...
W MediaStore: Failed to insert image
W MediaStore: java.lang.IllegalStateException: Failed to build unique file: /storage/emulated/0/Pictures/Image.jpg bucket_display_name=Pictures volume_name=external_primary date_modified=null date_expires=null _display_name=Image.jpg mime_type=image/jpeg _data=/storage/emulated/0/Pictures/Image.jpg _size=null is_trashed=0 is_pending=0 bucket_id=-1617409521 relative_path=Pictures/
W MediaStore: at android.os.Parcel.createExceptionOrNull(Parcel.java:2381)
W MediaStore: at android.os.Parcel.createException(Parcel.java:2357)
...
W MediaStore: at android.provider.MediaStore$Images$Media.insertImage(MediaStore.java:2097)
このログから、エラーがMediaStore.Images.Media.insertImage()の内部で発生しており、「Failed to build unique file」というメッセージが鍵であることがわかります。
問題の原因分析
java.lang.IllegalStateException: Failed to build unique fileというメッセージは、システムが新しいファイルを保存しようとした際に、一意のファイル名を生成できなかったことを示しています。
MediaStore.Images.Media.insertImage()メソッドは、画像のビットマップやパスを基にシステムギャラリーに画像を登録する簡易的なヘルパーです。このメソッドの引数にファイル名(display name)をnullとして渡すと、システムはデフォルトで「Image.jpg」のような汎用的なファイル名を自動的に割り当てようとします。もし同じディレクトリに同名のファイルが存在する場合、システムは自動的にサフィックスを追加してユニークなファイル名を生成します。例えば、「Image.jpg」があれば次に「Image(1).jpg」、「Image(2).jpg」というように連番を振ります。
しかし、この自動的なユニーク名生成メカニズムには隠れた上限が存在します。Androidの内部実装、特にMediaProviderが利用するファイルユーティリティ(FileUtils.buildUniqueFileWithExtension()のようなメソッド)では、この括弧付きの連番サフィックス(例:(32))の生成数に上限が設けられています。一般的に、この上限は32個(つまり、Image.jpgからImage(32).jpgまで、合計33個のファイル)とされており、これを超えるユニークなファイル名が必要になった場合に、上記のエラーIllegalStateExceptionがスローされます。
つまり、アプリケーションがファイル名を指定せずにinsertImage()を繰り返し呼び出し、同一ベース名の画像が大量に作成された結果、システムのユニーク名生成能力が限界に達したことが、エラーの根本原因です。
解決策:ユニークなファイル名の明示的な指定
この問題を解決するには、MediaStoreに画像を保存する際に、アプリケーション側で明示的にユニークなファイル名を指定することが不可欠です。MediaStore.Images.Media.insertImage()はシンプルですが、より詳細な制御と将来の互換性を考慮すると、ContentResolverとContentValuesを使用する方法が推奨されます。
以下に、現在のタイムスタンプを基にユニークなファイル名を生成し、ContentValuesを利用してMediaStoreに画像を挿入する例を示します。これにより、システムの自動命名機能による上限に依存することなく、確実にユニークなファイルを保存できます。
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class MediaStoreFileSaver {
/**
* Bitmap画像をMediaStoreに保存し、ギャラリーに表示されるようにします。
* 明示的にユニークなファイル名を指定することで、ファイル生成エラーを回避します。
*
* @param resolver ContentResolverインスタンス
* @param imageBitmap 保存するBitmap画像
* @param subFolder アプリ固有のサブフォルダ名(例: "MyAppImages")。nullまたは空の場合はPictures直下。
* @return 保存された画像のUri、または失敗した場合はnull
*/
public static Uri saveBitmapToGallery(ContentResolver resolver, Bitmap imageBitmap, String subFolder) {
// タイムスタンプを基にユニークなファイル名を生成
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String uniqueFileName = "IMG_" + timestamp + ".jpg";
String mimeType = "image/jpeg";
ContentValues mediaDetails = new ContentValues();
mediaDetails.put(MediaStore.Images.Media.DISPLAY_NAME, uniqueFileName);
mediaDetails.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
mediaDetails.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
mediaDetails.put(MediaStore.Images.Media.DATE_MODIFIED, System.currentTimeMillis() / 1000);
Uri imageCollection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10 (API 29)以降のScoped Storage対応
String relativePath = Environment.DIRECTORY_PICTURES;
if (subFolder != null && !subFolder.isEmpty()) {
relativePath += "/" + subFolder;
}
mediaDetails.put(MediaStore.Images.Media.RELATIVE_PATH, relativePath);
mediaDetails.put(MediaStore.Images.Media.IS_PENDING, 1); // 書き込み中は保留状態にする
imageCollection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
} else {
// API 28以下の従来のパス指定
// この方式はAPI 29以降で非推奨または動作しない可能性があります。
@SuppressWarnings("deprecation")
String picturesDirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString();
if (subFolder != null && !subFolder.isEmpty()) {
picturesDirPath += java.io.File.separator + subFolder;
}
java.io.File directory = new java.io.File(picturesDirPath);
if (!directory.exists() && !directory.mkdirs()) {
// ディレクトリ作成失敗をハンドリング
return null;
}
java.io.File targetFile = new java.io.File(directory, uniqueFileName);
mediaDetails.put(MediaStore.Images.Media.DATA, targetFile.getAbsolutePath());
imageCollection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
}
Uri finalImageUri = null;
OutputStream outputStream = null;
try {
finalImageUri = resolver.insert(imageCollection, mediaDetails);
if (finalImageUri == null) {
// MediaStoreへのエントリ挿入に失敗
return null;
}
outputStream = resolver.openOutputStream(finalImageUri);
if (outputStream == null) {
// OutputStreamの取得に失敗。作成したエントリを削除してクリーンアップ。
resolver.delete(finalImageUri, null, null);
return null;
}
// BitmapをOutputStreamに圧縮・書き込み
imageBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
outputStream.flush();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Scoped Storage: 書き込み完了後、保留状態を解除
mediaDetails.clear();
mediaDetails.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(finalImageUri, mediaDetails, null, null);
}
return finalImageUri; // 成功したUriを返す
} catch (IOException e) {
e.printStackTrace();
// エラー発生時、作成したMediaStoreエントリがあれば削除してクリーンアップ
if (finalImageUri != null) {
resolver.delete(finalImageUri, null, null);
}
return null;
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
このアプローチにより、開発者はシステムに依存せず、常にユニークなファイル名を確保できるため、IllegalStateException: Failed to build unique fileの発生を効果的に防ぐことができます。