概要
Android アプリ開発をしていると、しばしば画像ファイルを扱う場面が訪れます。
私が製作している Android アプリ SobaCha においても、画像ファイルを Twitter へ投稿する為の機能が存在しています。この画像投稿機能を改修している際に、Android の持つ画像ファイルの独特な扱い方と対面しました。
そこで私がつまづいてしまった点である File
と Uri
の関係をまとめ、後々調べる方のお役に立てればと思い書いてみました。
要約
画像ファイルはなるべく画像データが必要になる瞬間まで Uri
で扱い、最後に Uri
から InputStream
を開いて Bitmap
を取得しましょう。途中でファイルのパスを得ようとしてはいけません。
Android における画像ファイル
Android において、画像ファイルを扱う際に使用するクラスは 2 種類存在します。
File
Uri
画像データを扱う場合は、これらのインスタンスから InputStream
を取得し、画像を Bitmap
として取り出します。
File
とは
java.io.File
という名称の通り、ファイルを取り扱う Java の標準的なクラスです。
Android では内部ストレージや microSD 上のファイルを指定できます。
File
インスタンスのメソッドを使用すると、ファイルの絶対パスやファイルサイズの取得が可能です。
Uri
とは
android.net.Uri
という名称の通り、URI 形式の表記を取り扱う為に Android 用に作成されたクラスです。
URI は主に スキーム、オーソリティ、パス の 3 要素から構成されます。
画像ファイルの URI には、file://
スキームから始まる URI と、content://
スキームから始まる URI の 2 種類が存在します。
file://
スキーム
-
file:///storage/emulated/0/Pictures/1460293547698.jpg
- スキーム:
file://
- オーソリティ:
- パス:
/storage/emulated/0/Pictures/1460293547698.jpg
- スキーム:
file://
スキームの URI は、ファイルのパスを直接表現しています。
content://
スキーム
-
content://media/external/images/media/33612
- スキーム:
content://
- オーソリティ:
media
- パス:
/external/images/media/33612
- スキーム:
Android には ContentProvider
という、SQLite データベースに格納されている情報を他のアプリと共有する仕組みがあります。ContentProvider
を利用して格納された情報は ContentResolver
を使用すると取り出す事が出来ます。
また、Android には画像や音楽ファイルなどのメディア情報を収集する MediaScanner
という仕組みがあり、収集した情報は ContentProvider
を利用した MediaStore
というデータベースに格納されます。MediaStore
に格納されたメディア情報も、ContentResolver
経由で取り出す事が可能です。
この ContentResolver
を使用してメディア情報を取り出す際に、content://
スキームの URI で表記された Uri
を使用します。
content://
スキームの URI を用いると、内部ストレージに限らず、Google ドライブなどのクラウド上の画像を取り出す事も可能です。
相互変換
File
→ Uri
可能です。ただし生成した URI は外部への共有が制限される場合があります。
URI が共有可能かについては targetSdkVersion
に依存します。
Android 6.0 まで
Uri
クラスのメソッド fromFile(File)
を使用します。
File file = new File(path);
Uri uri = Uri.fromFile(file);
uri
には file://
スキームの URI が格納されます。
Android 7.0 以上
Android 7.0 以上の環境でも、fromFile(File)
メソッドで file://
スキームの Uri
を生成する事が可能です。
ただし、Android 7.0 以降向けにビルドしたアプリでは、上記の手法で生成した URI を他のアプリへと共有する事は出来ません。具体的には、アプリの targetSdkVersion
が 24
以上の場合、 file://
スキームの URI を Intent
で共有すると FileUriExposedException
が発生します。
詳細は Android 7.0 の動作の変更点 | Android Developers を参照してください。
簡単に解説すると、
- Android 7.0 以降向けのアプリでは、アプリデータの漏洩を防ぐために、プライベート ディレクトリのアクセス権限を厳しくした
-
file://
スキームの URI を使用すると、任意のパスを指定することが可能 - すなわち
file://
スキームの URI では、上記のプライベート ディレクトリのような、受け取った側がアクセスできないディレクトリを指定してしまう可能性がある - そのため、Android 7.0 以降向けのアプリでは
file://
URI の共有を禁止した
という経緯です。
共有の制限を解決する方法として、FileProvider
クラスのメソッド getUriForFile(Context, String, File)
を使用し、content://
スキームの URI を発行する方法があります。この手法では
Context.getFilesDir()
Context.getCacheDir()
Environment.getExternalStorageDirectory()
のディレクトリに保存されているファイルであれば、File
から Uri
を生成し、外部へと共有する事が出来ます。
ここでは例として、Context.getFilesDir()
内の document/share_file
を対象に、共有可能な Uri
を生成します。
まず、共有するディレクトリの情報を paths.xml
に指定します。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="document" path="document/" />
</paths>
ここで、共有するディレクトリと XML で使用するタグの間には、次の関係が存在します。
-
Context.getFilesDir()
...<files-path>
タグで指定 -
Context.getCacheDir()
...<cache-path>
タグで指定 -
Environment.getExternalStorageDirectory()
...<external-path>
タグで指定
次に、AndroidManifest.xml
の <application>
内に、FileProvider
に関する情報を記述します。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths"/>
</provider>
最後に、getUriForFile
メソッドを使用して、File
から Uri
を生成します。
File documentDir = new File(context.getFilesDir() + "/document");
File shareFile = new File(documentDir + "/share_file");
Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", shareFile);
上記ディレクトリ一覧以外に存在するファイルを共有するには、一度 Context.getCacheDir()
で取得したキャッシュ ディレクトリに外部のファイルをコピーします。コピーしたファイルを getUriForFile
メソッドに指定し、content://
スキームの URI を生成した後に外部のアプリへと共有します。
上記の実装は、以下の Web サイトを参考にさせて頂きました。
Uri
→ File
不可能な場合があります。
File
へ変換可能かは URI に依存します。
file://
スキーム
スキームが file://
の場合は、URI のパスがそのままファイルの位置を指しています。
そこで Uri
の getPath
メソッドを使用してパスを取得し、得られたパスを元に File
インスタンスを生成します。
File file = new File(uri.getPath())
content://
スキーム
スキームが content://
の場合は、URI のパスからファイルの位置を直接得る事はできません。
この時、URI の形式が
content://media/external/images/media/コンテンツID
の場合は、MediaStore
に対し問い合わせを行い、ファイルパスを文字列として得ます。
String[] projection = {MediaStore.MediaColumns.DATA};
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
String path = null;
if (cursor.moveToFirst()) {
path = cursor.getString(0);
}
cursor.close();
if (path != null) {
File file = new File(path);
}
}
ただし、同じ content://
スキームを持つ URI であっても
-
content://com.android.providers.media.documents/document/コンテンツID
…… システムの画像選択ウィンドウ -
content://com.android.providers.downloads.documents/document/コンテンツID
…… ダウンロード -
content://com.google.android.apps.docs.storage/document/コンテンツID
…… Google ドライブ -
content://com.dropbox.android.FileCache/filecache/コンテンツID
…… Dropbox
のような形式の URI である場合は、上記の手法でファイルパスを取得する事は出来ません。
上記の URI のうち、画像選択ウィンドウとダウンロードの 2 項目については、ファイルパスを取得する手法が StackOverflow に掲載されています。
一方、Google ドライブや Dropbox の URI からファイルパスを取得する方法はありません。
そのため、ファイルパスが取得出来る前提で Uri
を取り扱うのは避けるべきです。
InputStream
と Bitmap
の取得
File
→ InputStream
File
を渡して FileInputStream
を生成します。
InputStream stream = new FileInputStream(file);
Uri
→ InputStream
ContentResolver
インスタンスのメソッド openInputStream(Uri)
を使用します。
InputStream stream = context.getContentResolver().openInputStream(uri);
InputStream
→ Bitmap
InputStream
から Bitmap
を生成するには、BitmapFactory
クラスのメソッド decodeStream(InputStream)
を使用します。BufferedInputStream
を間に挟むことで、読み取り効率が向上します。
実際には、生成する Bitmap
のサイズを制限する為に BitmapFactory.Options
を渡すべきですが、ビットマップのデコードについては他に詳しい記事が存在するため詳細は割愛します。
Bitmap bitmap = BitmapFactory.decodeStream(new BufferedInputStream(stream));
ファイル名の取得
File
File
インスタンスのメソッド getName
を使用します。
String name = file.getName();
Uri
file://
スキーム
Uri
のパスを渡して File
インスタンスを生成し、getName
メソッドを呼び出します。
String name = new File(uri.getPath()).getName();
content://
スキーム
MediaStore
形式の URI の場合は MediaStore
に問い合わせを行います。
String[] projection = {MediaStore.MediaColumns.DISPLAY_NAME};
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
String name = null;
if (cursor.moveToFirst()) {
name = cursor.getString(0);
}
cursor.close();
}
ファイルサイズの取得
File
File
インスタンスのメソッド length
を使用します。
long size = file.length();
Uri
file://
スキーム
Uri
のパスを渡して File
インスタンスを生成し、length
メソッドを呼び出します。
long size = new File(uri.getPath()).length();
content://
スキーム
MediaStore
形式の URI の場合は MediaStore
に問い合わせを行います。
String[] projection = {MediaStore.MediaColumns.SIZE};
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
long size = 0;
if (cursor.moveToFirst()) {
size = cursor.getLong(0);
}
cursor.close();
}
写真の向きの取得
カメラアプリで撮影した画像ファイルをアプリから利用する際、写真撮影時の端末の向きによっては、そのまま Bitmap
を生成すると誤った向きの画像を生成してしまう恐れがあります。そこで、 JPEG ファイルの Exif 領域から写真の向きを取得し、Bitmap
を正しい向きへと回転します。
File
ファイルの絶対パスを ExifInterface
に渡し、インスタンスを生成します。
ExifInterface
から TAG_ORIENTATION
属性を取得し、属性の値を元に画像の向きを決定します。
ExifInterface exif = new ExifInterface(file.getAbsolutePath());
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
int orientation = 0;
switch (exifOrientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
orientation = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
orientation = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
orientation = 270;
break;
}
Uri
Uri
から写真の向きを得る手段は、以下の 2 種類が存在します。
-
MediaStore
に問い合わせを行なう -
InputStream
から画像ファイルの Exif 領域を読み取る
前者は MediaStore
に対し MediaStore.Images.ImageColumns.ORIENTATION
列を問い合わせる事で画像の向きの取得が可能です。ただし、システムの画像選択ウィンドウを始めとする MediaStore
形式以外の URI では、写真の向きが全て「 0° 」扱いにされてしまうため、あまり実用的な方法ではありません。
そこで、後者の手法について解説します。これまで、InputStream
から Exif を取得する為の ExifInterface(InputStream)
コンストラクタは API Level 24 すなわち Android 7.0 以降でのみ利用可能 でしたが、2017年 1月に ExifInterface Support Library が登場した為、Android 2.3 〜 6.0 の端末でも安心して InputStream
から Exif の情報を得られるようになりました。
ExifInterface Support Library を使用する為に、build.gradle
に依存ライブラリを追加します。バージョンは適宜最新の物に合わせてください。
compile 'com.android.support:exifinterface:25.1.1'
これで、ExifInterface(InputStream)
コンストラクタの使用が可能になります。
ExifInterface exif = new ExifInterface(new BufferedInputStream(stream));
int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
int orientation = 0;
switch (exifOrientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
orientation = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
orientation = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
orientation = 270;
break;
}
まとめ
Android では、幅広い方法で提供されている画像ファイルの情報を Uri
で束ねているため、最初はかなり扱いに戸惑いますが
- 画像ファイルは徹底して
Uri
で扱い、ファイルパスを取り出そうとしない - 画像データが必要になる直前で
InputStream
を取得する
事を心がけると、ContentProvider
に振り回されずに済むと思います。
必要に応じて、InputStream
から各種情報を得るサードパーティ ライブラリを使用すると良いでしょう。
検証用アプリ
記事を執筆するにあたり Uri
の各種検証の為に作成したテストアプリを GitHub に公開しました。
https://github.com/wakamesoba98/UriAnalyzer
参考
- The CommonsBlog - How to Consume Content From a Uri
- FileProvider | Android Developers
- Android 7.0 (API 24) への対応 ~(外部)ストレージ内ファイルの共有~ - きっとラボ
- Glide のメソッドをお借りして画像の向き(orientation)を取得する - 炊きたてのご飯が食べたい
- Android: Bitmaps loaded from gallery are rotated in ImageView - Stack Overflow
- Android getting orientation from MediaStore Uri - Stack Overflow
- android - cursor didn't have _data column not found - Stack Overflow
- Get real path from URI, Android KitKat new storage access framework - Stack Overflow