注意! この記事はQiitaにて公開されていた内容をimportしたものです。
これらの内容は場合によっては陳腐化していて役に立たなくなっていたり、有害であったり、現在の著者の主張と異なることがあります。
皆様の判断の上でご利用いただけますと幸いです(度を超してヤバいものは著者に連絡して頂ければ対応します m(_ _)m)


はじめに

タイトルは限定的に書いていますが、これはURI Permissionを扱うときには常に考慮しなければならない問題だと思われます。

共有で受け取ったデータを開く

ギャラリーアプリから自分のアプリに画像を取り込み、アレコレするという話はよくやる操作です。 ただ、Androidのギャラリーアプリから取得されたUriの指し示す場所は、いつも端末内(fileスキーマ)であるとは限りません。PicasaやGoogle+ Photoのクラウド上にのみ存在するファイルをソースとして扱うこともあります。

これらの複雑なソースの元がどこかを(あまり)考慮することなく、Uriを引数に与えるとInputStreamクラスで返却してくれる便利なメソッドとして、ContentResolver#openInputStream(Uri)があります。

public class PicasaTask extends AsyncTask<Uri, Void, Bitmap> {
    private Context context;

    public PicasaTask(Context context) {
        this.context = context;
    }

    @Override
    protected Bitmap doInBackground(Uri... params) {
        try {
            InputStream is = context.getContentResolver().openInputStream(params[0]);
            return BitmapFactory.decodeStream(is);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}
public class MyActivity extends Activity {
    private static final String TAG = "MyActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Intent intent = getIntent();
        String action = getIntent().getAction();
        String type = getIntent().getType();

        Log.d(TAG, "action:" + action + " /type:" + type);

        Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);

        if (imageUri != null) {

            new PicasaTask(this) {
                @Override
                protected void onPostExecute(Bitmap bitmap) {
                    super.onPostExecute(bitmap);

                    ImageView imageView = new ImageView(MyActivity.this);
                    imageView.setImageBitmap(bitmap);
                    setContentView(imageView);

                }
            }.execute(imageUri);
        }
    }
}

(AndroidManifestなどで、ACTION_SEND, CATEGORY_DEFAULT, mimeType=“image/*“を指定してあると想定します)

適当なギャラリーアプリから共有で上記コードを含むアプリを実行すると、Activityに画像が表示されることでしょう。

症状

ところが、実際のアプリでEXTRA_STREAMなUriを更に別のActivityに飛ばした後に画像取得しようとすると、以下のエラーが発生します。

07-23 10:25:37.441  21926-22655/com.example.picasatest E/AndroidRuntime﹕ FATAL EXCEPTION: AsyncTask #5
    Process: com.example.picasatest, PID: 21926
    java.lang.RuntimeException: An error occured while executing doInBackground()
            at android.os.AsyncTask$3.done(AsyncTask.java:300)
            at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
            at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
            at java.util.concurrent.FutureTask.run(FutureTask.java:242)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:841)
     Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.gallery3d.provider.GalleryProvider from ProcessRecord{43bf86e8 21926:com.example.picasatest/u0a156} (pid=21926, uid=10156) requires com.google.android.gallery3d.permission.GALLERY_PROVIDER or com.google.android.gallery3d.permission.GALLERY_PROVIDER
            at android.os.Parcel.readException(Parcel.java:1465)
            at android.os.Parcel.readException(Parcel.java:1419)
            at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)
            at android.app.ActivityThread.acquireProvider(ActivityThread.java:4399)
            at android.app.ContextImpl$ApplicationContentResolver.acquireUnstableProvider(ContextImpl.java:2200)
            at android.content.ContentResolver.acquireUnstableProvider(ContentResolver.java:1425)
            at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1047)
            at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:904)
            at android.content.ContentResolver.openInputStream(ContentResolver.java:629)
            at com.example.picasatest.android.lib.util.ImageUtil.resizeToFileFromOtherProvider(ImageUtil.java:129)
            at com.example.picasatest.android.lib.util.ImageUtil.resizeToFile(ImageUtil.java:57)
            at com.example.picasatest.util.ImageResizeTask.doInBackground(ImageResizeTask.java:53)
            at com.example.picasatest.util.ImageResizeTask.doInBackground(ImageResizeTask.java:14)
            at android.os.AsyncTask$2.call(AsyncTask.java:288)
            at java.util.concurrent.FutureTask.run(FutureTask.java:237)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:841)

ある処理を行うActivity(MyActivityとします)があり、そのActivityの肥大化を防ぐために、Intentを受け取る別のActivity(ShareActivityとします)を用意する手法をとると、このようなケースに陥りやすいのかと思います。

原因

こういう情報を得ましたが、launchMode=“singleTask"の設定や、singleTaskで起動するような設定を施していなかったので、ちょっと違うようです(とはいえこれも考慮すべき事でしょう)。

いろいろと調査した結果、受け取ったUriの有効期限が、「受け取ったActivityが終了するまで」となっているようです。 つまり、ShareActivityからMyActivityにUriを渡したあとでShareActivityがfinish()などで破棄されると、MyActivityが受け取ったUriは無効となり、アクセスした時にSecurityExceptionを出します。

これは通常のUriでは発生しません。Picasaやアドレス帳のようなプライベートな情報を扱えるアプリが有り、これらの情報にアクセスするパーミッションを持っていないアプリに一時的に権限を付与することができるUri Permissionsの仕組みを利用しています。

この有効期限が、直接Uriを受け取ったActivityのライフサイクルの終了までらしいということです。 らしいとしたのは、動きから推測しただけであり、僕が該当するコードや明記された資料を見つけられていないためです。

対策

ShareActivtyとMyActivityの構成を保ったまま解決する方法としては、以下の方法があると思います。

  • ShareActivityの生存期間を伸ばす
  • ShareActivityで画像取得アプリのcache領域などに出力し、fileスキーマなどで渡す

生存期間を伸ばすやり方については、以下の様な実装を行えばよいかと思います。しかし、開発者オプションの「アクティビティを保持しない」が有効であったり、メモリ不足になればActivityが破棄されてメンバ変数の値が揮発するでしょう。 よって、ShareActivityで画像を取得する方法がベターかなぁ、という気はします。

しかし、個人的には、最良の方法はMyActivityでギャラリーからのIntentを受け取ることだと思います。

private boolean isSent = false;

...

@Override
protected void onResume() {
    super.onResume();
    // 
    if (isUriSent) {
        finish();
        return;
    }
....
}

private void openActivity() {
    startActivity(intent);
    isUriSent = true;
}