KitKat:4.4でimage選択intentを利用する

はじめに




このエントリは Android Advent Calendar 2013 の9日目(12/9)の記事になります。

KitKatで搭載されたStorage Access Frameworkを使ったアプリからの画像選択の取り扱い方法
全然記事みつけられなくてて苦労したので少しでも参考になればとおもって書きます。


本題



さてAndroid4.4からStorage Access Framework(SAF)が導入されましたね
私が作っているアプリでもintentを発行して画像を選択するアプリを作っていたのですが

1.画像を選択
2.com.android.camera.action.CROPでクロップ処理のIntentを発行
3.投げた先のアプリでで強制終了してしまうという結末に

最初はギャラリーとかアルバムアプリを疑いました。(遷移先アプリで強制終了したので)

でもギャラリーとかがおかしいとかないよねとおもって調べたのですが
Intentの呼び出しとResultは以下のようなコードで書いていました
protected void showPickerIntent(){
        Intent intent = new Intent();
        intent.setType("image/*");
        intent.setAction(Intent.ACTION_GET_CONTENT);
        startActivityForResult(intent, PICKER_INTENT);
}
こんな感じでintentが発行されて画面が表示されます
Screenshot_2013-12-03-21-16-03

この時点でお分かりだと思いますが4.3以前の場合ですとギャラリー・アルバムなどの写真選択ができるアプリしか
Intentの対象になっていませんでしたが4.4ですと状況が一変しています。
DropboxやESファイルエクスプローラなどのファイルが扱えたりするもの、はたまたGoogleDriveまで選択できます。
(SAFのプロバイダを提供しているアプリがでているんですかね)

さて選択画面で画像を選択してonActivityResultで戻り値を受け取ります。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
     case PICKER_INTENT:
         if (resultCode == Activity.RESULT_OK){
             if(data == null || data.getData() == null) {
                return;
             }
         Toast.makeText(context,data.getData().toString(),Toast.LENGTH_SHORT).show();
     }
  }
}

Toastで中身を見てみます。
アルバムやギャラリーなどの写真表示系のアプリから写真を選択した場合は
getData()の中身はcontent://media/external/images/media/XXXXのような形式でURLが返却されます。
この場合はCROPへ処理を投げても強制終了はしませんでした(4.3系と同じですね)

ただしほかのアプリを選択した場合違うURLが返却されます。

私が確認した限りでは、
Dropbox
file:///storage/emulated/0/Android/data/com.dropbox.android/files/scratch/35_1_20120927183120.jpg
GoogleDrive
content://com.google.android.apps.docs.storage/document/acc%3D1%3Bdoc%3D204

このようなURLで返却されておりそのままCROPへIntent発行してしまうと対応しているURLじゃないので落ちてしまいます。
これで私のアプリが投げているパラメータが悪いということがわかりました。
 
 

どうやってDropboxやGoogleDriveのイメージに対応するか



実はDropBox(file://の場合)はDropboxで画像を選択した時点でDropboxアプリがサーバからデータを取得してくれるので単純にファイルとして扱えば問題なくCROPまでIntentを投げることができました。

問題はGoogleDriveです。

さっぱり記事が見つからず(見つけられず)いろいろ試行錯誤したのですが
content://から始まるのでContentResolverを利用したら取れるのかなと思ったら取れました。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
       switch (requestCode) {
            case PICKER_INTENT:
                if (resultCode == Activity.RESULT_OK){
                    if(data == null || data.getData() == null) {
                       Cursor cursor = getActivity().getContentResolver().query(uri,null, null, null, null);
                       cursor.moveToFirst();
                       int docIdIndex = cursor.getColumnIndex("document_id") ;
                       String fpath = cursor.getString(docIdIndex);
                       int index = cursor.getColumnIndex("mime_type");
                       String type = cursor.getString(index);
                       //TypeがImageのものだけ処理
                       if(type != null && type.contains("image")){
                       //GDrive処理
                       downloadPicker(data.getData());
                       return;
                    }
                }
            }
        }
}

public void downloadPicker(Uri uri){
       //非同期処理呼び出し
       new DownloadSAFFile(this.getActivity(),uri).execute();
}

public class DownloadSAFFile extends AsyncTask<Void,Void,File>{
      private Context context;
      private Uri uri;

      public DownloadSAFFile(Context context,Uri uri){
         this.context = context;
         this.uri = uri;
      }
      @Override
      protected File doInBackground(Void... params)
      {
            File cacheFile=new File(context.getExternalCacheDir(), "image_cache");
            try {
                        InputStream is = null;
                        //ここで取り出し
                        is = context.getContentResolver().openInputStream(uri);
                        OutputStream os = new FileOutputStream(cacheFile);
                        FileUtility.copyStream(is, os);
                        return cacheFile;
            } catch (Exception ex) {
                  // something went wrong
                  ex.printStackTrace();
                  return null;
            }
      }

      @Override
      protected void onPreExecute()
      {
      }

      @Override
      protected void onPostExecute(File file)
      {
            if(file != null){
            //file処理する
            //パラメータのファイルが実態なのであとはこのfileを扱えばOK
            }else{
                  Toast.makeText(context,"error", Toast.LENGTH_SHORT).show();
            }
      }
}
なぜ非同期なのかとなるのですがやはり外部へのアクセスになるのでデータのサイズやネットワーク状況によってダウンロードが遅い場合固まってしまうようにユーザから見えてしまうのでバックグラウンドで処理する必要があります。(あまり確認していないのですが長時間かかるとANRになるかも。また処理前と処理あとでダイアログなりなんなり上げてください)

これでGoogleDriveのデータもPickerで呼び出しできます。
 
 

残った課題



実は実装した際にダウンロード中にダイアログを上げるようにしたのですが、GoogleDrive上にあるものは
良いのですが、GoogleDriveからすでにダウンロードしてローカルにあるものについて実行すると
ダイアログが一瞬出て、すぐ閉じる(もちろんローカルにあるので)こととなってしまいました。
いろいろとコンテンツのUriのパラメータからダウンロード済みかどうか判断できるかどうかも調べたのですが
結果としてできませんでした。もしご存じの方いれば教えてください!
 
 

最後に



Storage Access Frameworkを利用すれば今まで簡単には使えなかったGoogleDriveへのファイルアクセスが簡単にできます。
ぜひぜひみなさんStorage Access Framework有効に使いましょう!

関連記事

コメントする

メモ - 下記に表示されているHTML属性は利用できます HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*