ストレージ権限を付与せずにカメラ撮影のIntentを投げる


ツイッター公式とかがランタイムパーミッション求めてくるけど、別になくてもいけるよな?って思ってやってみた。

やること

  • READ_EXTERNAL_STORAGE及びWRITE_EXTERNAL_STORAGE権限を付与しない
  • アプリからACTION_IMAGE_CAPTUREでアプリを起動する
  • 撮影された写真(Uri)をBitmapに復元する

実装

FileProviderを定義する

AndroidManigest.xml
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.provider"
    android:grantUriPermissions="true"
    android:exported="false">

    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_provider" />

</provider>
res/xml/file_provider.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">

    <files-path name="image" path="temp/"/>
</paths>

出力先のUriを用意する

ファイルを作ります

private File createOutputFile() {
    File tempFile = new File(context.getFilesDir(), "temp/sample");
    if (!tempFile.exists()) {
        try {
            tempFile.getParentFile().mkdirs();
            tempFile.createNewFile();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    return tempFile;
}

ファイルが存在していないと、カメラ側でFileNotFoundExceptionになるので空ファイルを作ってください。

作ったファイルをFileProviderを使ってUriに変換します。

FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file);

Intentを起動する

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

startActivityForResult(intent, REQUEST_CAPTURE);

結果を受け取る

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_CAPTURE && resultCode == RESULT_OK) {
        Uri uri = outputUri();
        Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
    }
}

ACTION_IMAGE_CAPTUREでは、出力先Uriが返却されない糞仕様なので、出力先Uriは起動側でどうにかして保持しておく必要があります。
Uriを動的に生成する場合、カメラ起動時にメモリ食ってActivityが破棄される可能性も考えるとonSaveInstanceState()を実装したりとそこそこめんどくさいですが、頑張ってください。

仕組み

出力先ファイルはアプリ領域に用意し、FileProviderを使ってUri化しています。

このUriは通常、当該アプリしかアクセスできませんが、IntentFLAG_GRANT_WRITE_URI_PERMISSIONフラグを追加することで、一時的に外部アプリにアクセス権限を与えることができます。

課題

KitKat以前は、暗黙的IntentにFLAG_GRANT_*_PERMISSIONが付与されない

暗黙的IntentにUriのアクセス権限が付与できないようです。

以下のようにすることで、付与できるようですが、ユーザーが起動するアプリ以外にも付与される気がするのでどうなんだろ…

List<ResolveInfo> resolves = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);

for (ResolveInfo resolveInfo : resolves) {
    grantUriPermission(resolveInfo.activityInfo.packageName, outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}

ちなみにgrantUriPermission()したら、必要に応じてrevokeしてあげたほうがいいみたいです。

revokeUriPermission(outputUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

参考

Setting Up File Sharing | Android Developers

Android:FileProviderでファイル共有 | Yukiの枝折