AndroidNのDropPermissionsを使ってアプリケーション間でDragAndDropする

Android N Developer Preview 1 いきなり発表されましたね。
http://developer.android.com/intl/ja/preview/index.html

現状の機能で最も派手で目立っているのは「Multi-Window」です。これは画面分割で2つのアプリケーションを並べて表示できる、というものですね。

f:id:funnelbit:20160401001419p:plain

ところでこのMulti-Windowには面白い機能があって、それはWindow間のDragAndDropです。画面分割して2つのアプリケーションが表示されている場合、その間をViewのDragAndDropをさせることが可能であるということです。またこのDragAndDropは、全く別のアプリケーションが対象であっても可能という面白い特徴を持っています。

シンプルなアプリケーション間のDragAndDrop

試しにシンプルなDragAndDropを試みてみます。テキストを相手に送るために、imageViewをDragAndDropしてみましょう。送る側、受け取り側、双方は別のアプリケーションとします。

送る側

imageView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                ClipData.Item item = new ClipData.Item("このテキスト渡したい");

                String[] mimeType = new String[1];
                mimeType[0] = ClipDescription.MIMETYPE_TEXT_PLAIN;

                ClipData clipData = new ClipData(new ClipDescription("text", mimeType), item);
                imageView.startDragAndDrop(clipData, new MyDragShadowBuilder(view), null, View.DRAG_FLAG_GLOBAL);

                return true;
            }
        });

受け取り側

root.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View view, DragEvent dragEvent) {
                ...
                if (dragEvent.getAction() == DragEvent.ACTION_DROP) {
                    if (dragEvent.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                        resultText.setText(dragEvent.getClipData().getItemAt(0).getText());
                    }
                 ...
            }
        });

ぱっと見ではこれまでのDragAndDropと変わりませんね。しかしいくつか新しいメソッドが登場しています。

startDragAndDrop()

startDragAndDrop(clipData, new MyDragShadowBuilder(view), view, View.DRAG_FLAG_GLOBAL);

 これは従来の startDrag() を置き換える形で新しく登場しました。これによって、startDrag() は非推奨となっています。
メソッドの利用法としては startDrag() とさほど大差がありませんが、注目すべきは第四引数の View.DRAG_FLAG_GLOBAL というflagです。

View.DRAG_FLAG_GLOBAL

 実態はただのint型定数です。View.DRAG_FLAG_GLOBALstartDragAndDrop() の第四引数に渡すことで、Windowを超えてのDragAndDropが可能になります。ここに渡すべきflagはいくつか種類があります。これは後述しますがDropPermissionsで非常に重要になります。

 上のコードでは登場しませんが、DragAndDropを停止する cancelDragAndDrop() や、Drag中の影を入れ替えることのできる updateDragShadow() なども用意されています。どちらもDrag操作を開始した側でしか呼べない点には注意してください。

 ちなみにDragが始まるとその瞬間、他のWindow内に対してDragEventのAction ACTION_DRAG_STARTED が伝達されます。Windowを跨がなくとも、他のアプリはすでにDragが開始されている事が分かるのです。

 ただのテキストやインテントを渡すだけなら、おそらくこれぐらいを知っていれば良いと思います。しかしこの機能で本当に面白いのはここからです。

DropPermissions

 アプリケーション間をまたいだDragAndDropが可能になったということは、受け取り側はそれに備えて色々考えなければなりません。その中でもとりわけ気になるのは「一体どんなデータが飛んで来るのか」ということだと思います。データによってはパーミッションが必要です。例えばアドレス帳のUriが渡された場合、処理するために READ_CONTACTS が必要です。
 
 どのパーミッションが必要なUriがどのタイミングで渡されてくるのか、アプリ開発者には検討がつきにくいと思います。可能であれば、Uriが渡された時にどんなパーミッションが必要か教えてくれると嬉しいですね。さらに欲を言えば、パーミッション周りの処理を勝手にやってもらえると非常に助かるというものです。

 そんな要望を叶えてくれるのがDropPermissionsです。DropPermissionは、Dropされた側がUriにアクセスするために必要なパーミッションを一時的に付与します。これが意味するのは、Dropされた側はパーミッションの実装が不用であるということです。

DropPermissionsを利用したDragAndDrop

 まずは送り側が「Uriを読み込む権限が必要です」と教えてあげる必要があります。その方法はこうです。

        imageView.setOnLongClickListener(new View.OnLongClickListener() {

            @Override
            public boolean onLongClick(View view) {
                Uri myPerson = Uri.withAppendedPath(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, "3");
                ClipData clipData = ClipData.newUri(getContentResolver(), "Uri", myPerson);
                imageView.startDragAndDrop(clipData, new MyDragShadowBuilder(view), view, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ );
                ...

 startDragAndDrop()の第四引数に注目してください。

 View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ

 としています。このメソッド複数のflagが渡ることを期待していますが、引数としては一つだけを受け付けます。 | で算出された値を渡すことでどのような状態を指定されているのか認識される仕組みです。

 View.DRAG_FLAG_GLOBAL_URI_READは単体で使えないという性質にも注意が必要です。View.DRAG_FLAG_GLOBAL_URI_READView.DRAG_FLAG_GLOBALと一緒に使われることを期待しています。

 もし書き込み権限が必要である場合は、View.DRAG_FLAG_GLOBAL_URI_WRITEを指定します。他にも幾つかのflagが用意されています。

 DropPermissionsrequestDropPermissions()メソッドによって作ることができます。これも Android N Developer Preview 1 からの新機能です。

        root.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View view, DragEvent dragEvent) {
                if (dragEvent.getAction() == DragEvent.ACTION_DROP) {

                    DropPermissions dropPermissions = requestDropPermissions(dragEvent);
                    getFrendNames(dragEvent.getClipData()) // ここでUriを元にデータを取得してる、とします。
               ...

 requestDropPermissions(dragEvent)によって、権限が必要である場合は付与されます。このメソッドから DropPermissions が返ってきた瞬間、このActivityが生きている限り自由に受け取ったUriに対してアクセスできます。もしも権限が必要ない場合や、なにかしらの理由で取得できない場合はnullを返すようになっています。この仕組みにより、受け取り側はAndroidManifestにパーミッションを定義する必要すらありません。

 Uriからデータを取得するために、返ってきた DropPermissions をどうこうすることはありません。ただし「もうこれ以上権限を保持する必要が無い」と判断したのならば、 release() メソッドを呼ぶことで破棄できます。

        root.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View view, DragEvent dragEvent) {
                if (dragEvent.getAction() == DragEvent.ACTION_DROP) {

                    DropPermissions dropPermissions = requestDropPermissions(dragEvent);
                    getFrendNames(dragEvent.getClipData()) // ここでUriを元にデータを取得してる、とします。
                    dropPermissions.release();
                    getFrendNames(dragEvent.getClipData()) // java.lang.SecurityException: Permission Denial
               ...

 ちなみに与えられたpermissionは、Dropされて得たUriのみに有効です。全く別のUriを組み立ててアクセス、というのは出来ません。

所感

 非常に面白い機能であると思います。Multi-Windowありきの機能ですのでMulti-Windowどれだけ使われるのかがポイントになりますが、Androidの強みであるアプリ間の連携がさらに強化されているなという印象です。ひょっとするとAndroidは、ある程度複雑な仕事にも使えるパワフルなOSを目指しているのかもしれません。
 まだPreview 1 ですので今後変更される可能性は大いにありますが、全く別になるということは無いと思います。正式リリースが待ち遠しいですね。