6 月 3 日の「#Android11: The Beta Launch Show」にぜひご参加ください。

イメージ キーボードのサポート

図 1: イメージ キーボード サポートの例

多くのユーザーは、絵文字やステッカーなど、さまざまなリッチ コンテンツを使用してコミュニケーションができることを望んでいます。以前のバージョンの Android の場合、ソフト キーボード(インプット メソッド エディタ、IME)でアプリに送信できるのは、Unicode の絵文字だけに限られていました。リッチ コンテンツに関しては、別のアプリでは使用できない各アプリ固有の API を作成するか、Easy Share Action やクリップボードを通じて画像を送信するなどの回避策を使用する必要がありました。

Android 7.1(API レベル 25)の Android SDK には、Commit Content API が含まれています。この API により、IME は、アプリ内のテキスト エディタに画像や各種リッチ コンテンツを直接送信できるようになります。この API は、リビジョン 25.0.0 以降の v13 サポート ライブラリでも利用できます。このサポート ライブラリは Android 3.2(API レベル 13)以降のデバイス上で実行可能で、実装を簡素化するヘルパー メソッドが含まれているため、サポート ライブラリを使用することをおすすめします。

この API を使用すると、任意のキーボードからリッチ コンテンツを受け入れるメッセージ アプリや、リッチ コンテンツを任意のアプリに送信できるキーボードを作成できます。Google キーボードGoogle Messenger などのアプリは、Android 7.1 の Commit Content API をサポートしています(図 1 を参照)。

このページでは、IME とアプリの両方で Commit Content API を実装する方法について説明します。

仕組み

キーボードによる画像挿入は、IME とアプリの両方が対応している必要があります。画像挿入プロセスの各ステップは次のとおりです。

  1. ユーザーが EditText をタップすると、エディタは、EditorInfo.contentMimeTypes を使用して、受け入れ可能な MIME コンテンツ タイプのリストを送信します。

  2. IME は、サポートされているタイプのリストを読み取り、エディタが受け入れ可能なコンテンツをソフト キーボード内に表示します。

  3. ユーザーが画像を選択すると、IME は commitContent() を呼び出し、InputContentInfo をエディタに送信します。commitContent() 呼び出しは、commitText() 呼び出しと似ていますが、リッチ コンテンツ用です。InputContentInfo には、コンテンツ プロバイダ内のコンテンツを識別する URI が格納されます。アプリはパーミッションをリクエストして、URI からコンテンツを読み取ることができます。

アプリに画像サポートを追加する

アプリが IME からリッチ コンテンツを受け入れるには、受け入れ可能なコンテンツ タイプを IME に伝え、コンテンツの受信時に実行するコールバック メソッドを指定する必要があります。PNG 画像を受け入れる EditText を作成する方法の例を以下に示します。

Kotlin

    val editText = object : EditText(this) {

        override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
            val ic: InputConnection = super.onCreateInputConnection(editorInfo)
            EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png"))

            val callback =
                    InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts ->
                        val lacksPermission = (flags and
                                InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
                        // read and display inputContentInfo asynchronously
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
                            try {
                                inputContentInfo.requestPermission()
                            } catch (e: Exception) {
                                return@OnCommitContentListener false // return false if failed
                            }
                        }

                        // read and display inputContentInfo asynchronously.
                        // call inputContentInfo.releasePermission() as needed.

                        true  // return true if succeeded
                    }
            return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
        }
    }

    

Java

    EditText editText = new EditText(this) {
        @Override
        public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
            final InputConnection ic = super.onCreateInputConnection(editorInfo);
            EditorInfoCompat.setContentMimeTypes(editorInfo,
                    new String [] {"image/png"});

            final InputConnectionCompat.OnCommitContentListener callback =
                new InputConnectionCompat.OnCommitContentListener() {
                    @Override
                    public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
                            int flags, Bundle opts) {
                        // read and display inputContentInfo asynchronously
                        if (BuildCompat.isAtLeastNMR1() && (flags &
                            InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
                            try {
                                inputContentInfo.requestPermission();
                            }
                            catch (Exception e) {
                                return false; // return false if failed
                            }
                        }

                        // read and display inputContentInfo asynchronously.
                        // call inputContentInfo.releasePermission() as needed.

                        return true;  // return true if succeeded
                    }
                };
            return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
        }
    };

    

多くのことが行われているので、何が起こっているのかを説明しましょう。

  • この例ではサポート ライブラリを使用しているため、android.view.inputmethod ではなく、android.support.v13.view.inputmethod への参照があります。

  • この例では、EditText を作成し、onCreateInputConnection(EditorInfo) メソッドをオーバーライドして、InputConnection を変更しています。InputConnection は、IME とその入力を受け取るアプリとの間の通信チャネルです。

  • super.onCreateInputConnection() 呼び出しは、ビルトイン動作(テキストの送受信)を保持し、InputConnection への参照を提供します。

  • setContentMimeTypes() は、サポートされている MIME タイプのリストを EditorInfo に追加します。必ず、setContentMimeTypes() を呼び出す前に super.onCreateInputConnection() を呼び出してください。

  • IME がコンテンツをコミットするたびに、callback が実行されます。onCommitContent() メソッドには、コンテンツ URI を格納する InputContentInfoCompat への参照があります。

    • API レベル 25 以降のデバイス上でアプリが稼働していて、IME によって INPUT_CONTENT_GRANT_READ_URI_PERMISSION フラグが設定されている場合は、パーミッションのリクエストとリリースを行う必要があります。そうでない場合は、IME によってパーミッションが付与されているか、コンテンツ プロバイダがアクセスを制限していないため、すでにコンテンツ URI にアクセスできるはずです。詳細については、IME に画像サポートを追加するをご覧ください。
  • createWrapper() は、inputConnection、変更後の editorInfo、新しい InputConnection に対するコールバックをラップして返します。

推奨される方法は以下のとおりです。

  • リッチ コンテンツをサポートしていないエディタは、setContentMimeTypes() を呼び出さずに、EditorInfo.contentMimeTypesnull に設定したままにします。

  • InputContentInfo 内で指定されている MIME タイプが、受け入れ可能なタイプのいずれとも一致しない場合、エディタは、コンテンツを無視する必要があります。

  • リッチ コンテンツは、テキスト カーソルの位置には影響せず、テキスト カーソルの位置から影響を受けることもありません。コンテンツを操作する際、エディタはカーソルの位置を無視できます。

  • エディタの OnCommitContentListener.onCommitContent() メソッドを使用すると、コンテンツをロードする前であっても、非同期で true を返すことができます。

  • コミットする前に IME 内で編集できるテキストとは異なり、リッチ コンテンツはすぐにコミットされます。ユーザーがコンテンツを編集、削除できるようにする場合は、デベロッパー自身でロジックを実装する必要があります。

アプリをテストする際は、デバイスまたはエミュレータに、リッチ コンテンツを送信できるキーボードがあるか確認してください。Android 7.1 以降の場合は、Google キーボードを使用できます。

IME に画像サポートを追加する

リッチ コンテンツをアプリに送信する IME は、以下のように Commit Content API を実装する必要があります。

  • onStartInput() または onStartInputView() をオーバーライドして、サポートされているコンテンツ タイプのリストをターゲット エディタから読み取ります。ターゲット エディタが GIF 画像を受け入れるかどうかをチェックするコード スニペットを以下に示します。

Kotlin

    override fun onStartInputView(editorInfo: EditorInfo, restarting: Boolean) {
        val mimeTypes: Array<String> = EditorInfoCompat.getContentMimeTypes(editorInfo)

        val gifSupported: Boolean = mimeTypes.any {
            ClipDescription.compareMimeTypes(it, "image/gif")
        }

        if (gifSupported) {
            // the target editor supports GIFs. enable corresponding content
        } else {
            // the target editor does not support GIFs. disable corresponding content
        }
    }

    

Java

    @Override
    public void onStartInputView(EditorInfo info, boolean restarting) {
        String[] mimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);

        boolean gifSupported = false;
        for (String mimeType : mimeTypes) {
            if (ClipDescription.compareMimeTypes(mimeType, "image/gif")) {
                gifSupported = true;
            }
        }

        if (gifSupported) {
            // the target editor supports GIFs. enable corresponding content
        } else {
            // the target editor does not support GIFs. disable corresponding content
        }
    }

    
  • ユーザーが画像を選択したときに、アプリにコンテンツをコミットします。エディタがフォーカスを失う可能性があるため、テキストを入力している間は commitContent() を呼び出さないでください。GIF 画像をコミットするコード スニペットを以下に示します。

Kotlin

    /**
     * Commits a GIF image
     *
     * @param contentUri Content URI of the GIF image to be sent
     * @param imageDescription Description of the GIF image to be sent
     */
    fun commitGifImage(contentUri: Uri, imageDescription: String) {
        val inputContentInfo = InputContentInfoCompat(
                contentUri,
                ClipDescription(imageDescription, arrayOf("image/gif")),
                null
        )
        val inputConnection = currentInputConnection
        val editorInfo = currentInputEditorInfo
        var flags = 0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
            flags = flags or InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION
        }
        InputConnectionCompat.commitContent(inputConnection, editorInfo, inputContentInfo, flags, null)
    }

    

Java

    /**
     * Commits a GIF image
     *
     * @param contentUri Content URI of the GIF image to be sent
     * @param imageDescription Description of the GIF image to be sent
     */
    public static void commitGifImage(Uri contentUri, String imageDescription) {
        InputContentInfoCompat inputContentInfo = new InputContentInfoCompat(
                contentUri,
                new ClipDescription(imageDescription, new String[]{"image/gif"}),
                null
        );
        InputConnection inputConnection = getCurrentInputConnection();
        EditorInfo editorInfo = getCurrentInputEditorInfo();
        Int flags = 0;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
            flags |= InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
        }
        InputConnectionCompat.commitContent(
                inputConnection, editorInfo, inputContentInfo, flags, null);
    }

    
  • IME のデベロッパーは、ほとんどの場合、独自のコンテンツ プロバイダを実装して、コンテンツ URI リクエストに応答する必要があります。MediaStore のような既存のコンテンツ プロバイダからのコンテンツを IME がサポートすることもありますが、極めて例外的です。コンテンツ プロバイダの作成方法については、コンテンツ プロバイダファイル プロバイダをご覧ください。

  • 独自のコンテンツ プロバイダを作成している場合は、エクスポートしないことをおすすめします(android:exportedfalse に設定します)。代わりに、android:grantUriPermissiontrue に設定することで、プロバイダ内でパーミッションの付与を有効にしてください。IME は、コンテンツがコミットされると、コンテンツ URI にアクセスするパーミッションを付与できるようになります。これには、次の 2 つの方法があります。

    • Android 7.1(API レベル 25)以降の場合、commitContent() を呼び出すときに、フラグ パラメータを INPUT_CONTENT_GRANT_READ_URI_PERMISSION に設定します。これにより、アプリが受け取る InputContentInfo オブジェクトは、requestPermission()releasePermission() を呼び出すことで、一時的な読み取りパーミッションのリクエストとリリースができるようになります。

    • Android 7.0(API レベル 24)以前の場合、INPUT_CONTENT_GRANT_READ_URI_PERMISSION は無視されるため、手動でコンテンツにパーミッションを付与する必要があります。grantUriPermission() を使用する方法もありますが、独自の要件を満たす独自のメカニズムを実装することをおすすめします。

IME をテストする際は、デバイスまたはエミュレータに、リッチ コンテンツを受信できるアプリがあるか確認してください。Android 7.1 以降の場合は、Google Messenger アプリを使用できます。