先日のGoogle I/O 2012で行われた「Security and Privacy Android Apps」という講演について、軽く「パナソニックでの講演と、松下幸之助歴史館と、大阪駅」の記事でご紹介しましたが、その中で、パミッションをなるべく少なくする方法として、「Get a camera pic without CAMERA permission」というスライドがありましたので、それのご紹介です。

android.permission.CAMERAが必要なパターン

camera_permission1.png

android.permission.CAMERAパミッションが必要ないパターン

camera_permission2.png

既にこの機能については、色々な所で紹介されていますが、改めて色々なアプリを見てみると、Facebookアプリなどはこれ使ったらカメラパミッションいらなくなるのでは?なんて事を感じたりしました。既に既知情報ではありますが、パミッションを少なくする方法としてご紹介します。ちなみにTwicca等はこの手法で実現しています。

カメラパミッションなしに写真を取る方法

カメラを取るには、AndroidManifestにカメラパミッションの利用宣言をする必要があります。


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

画面にカメラの画像をリアルタイムに表示しながら、エフェクトを書けたり、顔認識をしたり、標準カメラにないUIを付加するような場合はカメラパミッションが必要です。しかしながら、Twitterアプリのように写真を取って投稿するだけのアプリケーションではカメラパミッションなしで機能実装が可能です。

カメラアプリが撮影後に保存する写真のファイル名をインテントに設定しし、カメラアプリをstartActivityResultで起動します。カメラアプリが起動され写真撮影後アプリケーションに戻ってきたタイミングで、onActivityResultが呼び出されます。この時点でインテントに指定したファイルに写真が撮影されています。ユーザが実際に写真を取ったのか、キャンセルしたのかは、resultCodeで取得可能です。

MediaStore.EXTRA_OUTPUT

カメラアプリによって保存されるファイル名(Uri)を指定します。以下のコードでは、SDカード上のPicturesディレクトリに、このアプリケーション専用のディレクトリを作成し、日付付きのjpgファイル名を渡しています。

MetiaStore.Extra_OUTPUTを指定しない場合は、onActivityResultのIntentに撮影画像が入っていますが、縮小された画像です。Android Developer GuideにはIntent.getData()で取得可能と記載されていますがnullを返す端末もあります。またIntent.getData()がnullを返す場合は、Bitmap bitmap = (Bitmap)data.getExtras().get(“data”)で縮小画像を取得できる事もあります。(Galaxy Nexus 4.04にて確認)このように仕様が不明瞭な事と取得できる画像が縮小画像のため、MediaStore.EXTRA_OUTPUTを使用する事をお勧めします。

カメラの呼び出し


private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
private Uri fileUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// create Intent to take a picture and return control to the calling application
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // create a file to save the image
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // set the image file name
// start the image capture Intent
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
}

getOutputMediaFileUriメソッド(後述)がSDカード上のファイル名を決定しています。写真はファイルサイズが大きいため通常外部記憶装置(SDカード等)に保存します。アプリケーションがファイルを外部記憶装置に保存する場合は以下の2つのメソッドを利用して保存先を決定します。


Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)

全てのアプリで共有されるディレクトリを返します。このメソッドで返されるディレクトリの下に、アプリケーション専用のディレクトリを作成して使用します。アプリケーションをアンインストールした後もファイルを残したい時に利用します。(Android OS 2.2 API LEVEL 8以上)

Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)

アプリケーションをアンインストールした時に同時に作成したファイルを削除したい時に、このディレクトリを使用します。

どちらのディレクトリを使用するにしても、外部記憶装置上なので、他のアプリから読み込み、書き込み、削除可能でりセキュリティ的に保護されていない事に注意してください。

以下のコードでは、Environment.getExtraStragePublicDirectoryを使用しています。取得したディレクトリの下に、「MyCameraApp」ディレクトリを作成し、日時を利用したファイル名を作成しています。どのようなファイル名になるかは機種によって異なりますが、Galaxy Nexus (Android OS 4.04)の場合は、/mnt/sdcard/Pictures/IntentCamera/IMG_20120717_085120.jpgといったファイル名が生成されます。

ファイル名を決定するメソッド


public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;
/** Create a file Uri for saving an image or video */
private static Uri getOutputMediaFileUri(int type){
return Uri.fromFile(getOutputMediaFile(type));
}
/** Create a File for saving an image or video */
private static File getOutputMediaFile(int type){
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "MyCameraApp");
// This location works best if you want the created images to be shared
// between applications and persist after your app has been uninstalled.
// Create the storage directory if it does not exist
if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()){
Log.d("MyCameraApp", "failed to create directory");
return null;
}
}
// Create a media file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File mediaFile;
if (type == MEDIA_TYPE_IMAGE){
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"IMG_"+ timeStamp + ".jpg");
} else if(type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"VID_"+ timeStamp + ".mp4");
} else {
return null;
}
return mediaFile;
}

Intentでカメラアプリを起動しているので、MediaStore.ACTION_IMAGE_CAPTUREアクションに対応しているアプリケーションがある場合はアプリケーション選択画面となります。

カメラアプリを起動し、写真を撮影、カメラアプリが終了すると、onActivityResultに戻ってきます。requestCodeやresultCodeを参照し適切に処理をします。MediaStore.EXTRA_OUTPUT
にUriを指定した場合は、この時点で撮影したファイルが外部記憶装置に書きこまれているので、画面に表示する事が可能です。

カメラアプリからの戻り


private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
private static final int CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE = 200;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
// Image captured and saved to fileUri specified in the Intent
Toast.makeText(this, "Image saved to:n" +
data.getData(), Toast.LENGTH_LONG).show();
} else if (resultCode == RESULT_CANCELED) {
// User cancelled the image capture
} else {
// Image capture failed, advise user
}
}
if (requestCode == CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
// Video captured and saved to fileUri specified in the Intent
Toast.makeText(this, "Video saved to:n" +
data.getData(), Toast.LENGTH_LONG).show();
} else if (resultCode == RESULT_CANCELED) {
// User cancelled the video capture
} else {
// Video capture failed, advise user
}
}
}

上記のコードでは、カメラでの写真撮影だけでなく、ビデオ撮影にも対応しています。ビデオ撮影の場合は、アクションにMediaStore.ACTION_VIDEO_CAPTUREを指定します。また撮影ファイル名だけではなく、クオリティやファイルサイズの上限等を指定する事も可能です。詳細については、Android Developer SiteのMedia and CameraのCameraの章を参考にしてください。

Android Developer Site Camera: http://developer.android.com/intl/ja/guide/topics/media/camera.html

このように、単に撮影するだけであれば、android.permission.CAMERAを利用宣言する必要はありません。カメラパミッションを持っているアプリは、ユーザが気が付かないように起動、撮影、終了を繰り返すような盗撮機能の実装が可能であり、マルウェア的な動作をする事が可能となりますので、出来れば付けない方が良いパミッションの一つです。
またカメラは機種依存が激しく全ての機種で動作させるのは非常に難しいでが、このようにインテントを利用してカメラアプリを起動することで機種依存問題を解決する事も可能となります。

その他

MetiaStore.Extra_OUTPUTにUriを指定しますが、ContentProviderのUri(contents://ではじまる)を渡すと旨く動作しない端末があるようです。このファイルパスから指定したUri(file://で始まる)すると動作するとありますので、特別な理由がない限りSDカード上のファイルを指定するのをお勧めします。

2011/6/30の情報では、シャープ製品の以下の端末で問題があります。
IS03 Froyo、SH-03C Froyo、003SH、DM009SH、005SH、IS05、SH-12C、006SH、007SH、IS11SH、IS12SH、A01

また2011/12/2の情報では、以下のシャープ端末では問題ないとの事ですので、今後の端末では問題が発生しないと推測されます。
101SH、IS13SH、SH-01D

参照 シャープのSH Developer Square
https://sh-dev.sharp.co.jp/android/modules/d3forum/index.php?topic_id=133

注)シャープ製品以外での製品においても同じような現象が出ると予測されますが、公式情報としてきちんと対応されているシャープの情報を記載させて頂きました

その他2

久しぶりに長いブログとなりました。いつ改定されるのか、はたして改訂する機会があるのか、まったくわかりませんが、Android Security本の6章に、「パミッションを使わない方法」というのが記載されているべきだと思って、本ベースになるように記載しました。

他のTipsについても、時間があるときに書いていこうと思ってます。

ブログ内の関連する記事