Add a demo for InputConnectionCompat#commitContent

This CL demonstrates what changes need to be done in application to
support IMEs that can commit contents such as GIF image via
InputConnectionCompat#commitContent() support library API (or
InputConnection#commitContent() API added in API 25).

Although this sample uses WebView to show the image sent from the IME to
make the sample code as simple as possible, the key part here is that
app developers will receive content URIs and it is up to them (and their
applications) how receved contents need to be handled.

Bug: 29833914
Change-Id: Ib0f927b553a1f557f4cf81747ae1cd72e371a287
diff --git a/samples/Support13Demos/AndroidManifest.xml b/samples/Support13Demos/AndroidManifest.xml
index dfa4e64..01bab26 100644
--- a/samples/Support13Demos/AndroidManifest.xml
+++ b/samples/Support13Demos/AndroidManifest.xml
@@ -87,5 +87,13 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".view.inputmethod.CommitContentSupport"
+                 android:label="@string/commit_content_support">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv13.SUPPORT13_SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
diff --git a/samples/Support13Demos/res/layout/commit_content.xml b/samples/Support13Demos/res/layout/commit_content.xml
new file mode 100644
index 0000000..414df18
--- /dev/null
+++ b/samples/Support13Demos/res/layout/commit_content.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent"
+             android:background="@android:color/black">
+
+    <WebView
+        android:id="@+id/commit_content_webview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@android:color/transparent" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="#77000000"
+        android:orientation="vertical">
+
+        <HorizontalScrollView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:fadeScrollbars="false"
+            android:scrollbars="horizontal">
+
+            <TableLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content">
+
+                <TableRow>
+
+                    <TextView
+                        android:layout_column="1"
+                        android:gravity="end"
+                        android:padding="3dip"
+                        android:text="MIME"
+                        android:textColor="@android:color/white"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/text_commit_content_mime_types"
+                        android:padding="3dip"
+                        android:textColor="@android:color/white" />
+                </TableRow>
+
+                <TableRow>
+
+                    <TextView
+                        android:layout_column="1"
+                        android:gravity="end"
+                        android:padding="3dip"
+                        android:text="Label"
+                        android:textColor="@android:color/white"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/text_commit_content_label"
+                        android:padding="3dip"
+                        android:textColor="@android:color/white" />
+                </TableRow>
+
+                <TableRow>
+
+                    <TextView
+                        android:layout_column="1"
+                        android:gravity="end"
+                        android:padding="3dip"
+                        android:text="URI"
+                        android:textColor="@android:color/white"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/text_commit_content_content_uri"
+                        android:padding="3dip"
+                        android:textColor="@android:color/white" />
+                </TableRow>
+
+                <TableRow>
+
+                    <TextView
+                        android:layout_column="1"
+                        android:gravity="end"
+                        android:padding="3dip"
+                        android:text="Link"
+                        android:textColor="@android:color/white"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/text_commit_content_link_uri"
+                        android:padding="3dip"
+                        android:textColor="@android:color/white" />
+                </TableRow>
+
+                <TableRow>
+
+                    <TextView
+                        android:layout_column="1"
+                        android:gravity="end"
+                        android:padding="3dip"
+                        android:text="Flags"
+                        android:textColor="@android:color/white"
+                        android:textStyle="bold" />
+
+                    <TextView
+                        android:id="@+id/text_commit_content_link_flags"
+                        android:padding="3dip"
+                        android:textColor="@android:color/white" />
+                </TableRow>
+            </TableLayout>
+        </HorizontalScrollView>
+
+        <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:fadeScrollbars="false"
+            android:scrollbars="vertical">
+
+            <LinearLayout
+                android:id="@+id/commit_content_sample_edit_boxes"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical" />
+        </ScrollView>
+
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/samples/Support13Demos/res/values/strings.xml b/samples/Support13Demos/res/values/strings.xml
index 92316d0..30b6a18 100644
--- a/samples/Support13Demos/res/values/strings.xml
+++ b/samples/Support13Demos/res/values/strings.xml
@@ -36,4 +36,7 @@
     <string name="fragment_state_pager_support">Fragment/State Pager</string>
 
     <string name="action_bar_tabs_pager">Fragment/Action Bar Tabs Pager</string>
+
+    <string name="commit_content_support">View/Input Method/Commit Content</string>
+
 </resources>
diff --git a/samples/Support13Demos/src/com/example/android/supportv13/view/inputmethod/CommitContentSupport.java b/samples/Support13Demos/src/com/example/android/supportv13/view/inputmethod/CommitContentSupport.java
new file mode 100644
index 0000000..29f8a91f
--- /dev/null
+++ b/samples/Support13Demos/src/com/example/android/supportv13/view/inputmethod/CommitContentSupport.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.example.android.supportv13.view.inputmethod;
+
+import com.example.android.supportv13.R;
+
+import android.support.v13.view.inputmethod.EditorInfoCompat;
+import android.support.v13.view.inputmethod.InputConnectionCompat;
+import android.support.v13.view.inputmethod.InputContentInfoCompat;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.webkit.WebView;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import static android.widget.LinearLayout.VERTICAL;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class CommitContentSupport extends Activity {
+    private static final String INPUT_CONTENT_INFO_KEY = "COMMIT_CONTENT_INPUT_CONTENT_INFO";
+    private static final String COMMIT_CONTENT_FLAGS_KEY = "COMMIT_CONTENT_FLAGS";
+
+    private static String TAG = "CommitContentSupport";
+
+    private WebView mWebView;
+    private TextView mLabel;
+    private TextView mContentUri;
+    private TextView mLinkUri;
+    private TextView mMimeTypes;
+    private TextView mFlags;
+
+    private InputContentInfoCompat mCurrentInputContentInfo;
+    private int mCurrentFlags;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.commit_content);
+
+        final LinearLayout layout =
+                (LinearLayout) findViewById(R.id.commit_content_sample_edit_boxes);
+
+        // This declares that the IME cannot commit any content with
+        // InputConnectionCompat#commitContent().
+        layout.addView(createEditTextWithContentMimeTypes(null));
+
+        // This declares that the IME can commit contents with
+        // InputConnectionCompat#commitContent() if they match "image/gif".
+        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/gif"}));
+
+        // This declares that the IME can commit contents with
+        // InputConnectionCompat#commitContent() if they match "image/png".
+        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/png"}));
+
+        // This declares that the IME can commit contents with
+        // InputConnectionCompat#commitContent() if they match "image/jpeg".
+        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/jpeg"}));
+
+        // This declares that the IME can commit contents with
+        // InputConnectionCompat#commitContent() if they match "image/webp".
+        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/webp"}));
+
+        // This declares that the IME can commit contents with
+        // InputConnectionCompat#commitContent() if they match "image/png", "image/gif",
+        // "image/jpeg", or "image/webp".
+        layout.addView(createEditTextWithContentMimeTypes(
+                new String[]{"image/png", "image/gif", "image/jpeg", "image/webp"}));
+
+        mWebView = (WebView) findViewById(R.id.commit_content_webview);
+        mMimeTypes = (TextView) findViewById(R.id.text_commit_content_mime_types);
+        mLabel = (TextView) findViewById(R.id.text_commit_content_label);
+        mContentUri = (TextView) findViewById(R.id.text_commit_content_content_uri);
+        mLinkUri = (TextView) findViewById(R.id.text_commit_content_link_uri);
+        mFlags = (TextView) findViewById(R.id.text_commit_content_link_flags);
+
+        if (savedInstanceState != null) {
+            final InputContentInfoCompat previousInputContentInfo = InputContentInfoCompat.wrap(
+                    savedInstanceState.getParcelable(INPUT_CONTENT_INFO_KEY));
+            final int previousFlags = savedInstanceState.getInt(COMMIT_CONTENT_FLAGS_KEY);
+            if (previousInputContentInfo != null) {
+                onCommitContentInternal(previousInputContentInfo, previousFlags);
+            }
+        }
+    }
+
+    private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
+            Bundle opts, String[] contentMimeTypes) {
+        // Clear the temporary permission (if any).  See below about why we do this here.
+        try {
+            if (mCurrentInputContentInfo != null) {
+                mCurrentInputContentInfo.releasePermission();
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "InputContentInfo#releasePermission() failed.", e);
+        } finally {
+            mCurrentInputContentInfo = null;
+        }
+
+        mWebView.loadUrl("about:blank");
+        mMimeTypes.setText("");
+        mContentUri.setText("");
+        mLabel.setText("");
+        mLinkUri.setText("");
+        mFlags.setText("");
+
+        boolean supported = false;
+        for (final String mimeType : contentMimeTypes) {
+            if (inputContentInfo.getDescription().hasMimeType(mimeType)) {
+                supported = true;
+                break;
+            }
+        }
+        if (!supported) {
+            return false;
+        }
+
+        return onCommitContentInternal(inputContentInfo, flags);
+    }
+
+    private boolean onCommitContentInternal(InputContentInfoCompat inputContentInfo, int flags) {
+        if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+            try {
+                inputContentInfo.requestPermission();
+            } catch (Exception e) {
+                Log.e(TAG, "InputContentInfo#requestPermission() failed.", e);
+                return false;
+            }
+        }
+
+        mMimeTypes.setText(
+                Arrays.toString(inputContentInfo.getDescription().filterMimeTypes("*/*")));
+        mContentUri.setText(inputContentInfo.getContentUri().toString());
+        mLabel.setText(inputContentInfo.getDescription().getLabel());
+        Uri linkUri = inputContentInfo.getLinkUri();
+        mLinkUri.setText(linkUri != null ? linkUri.toString() : "null");
+        mFlags.setText(flagsToString(flags));
+        mWebView.loadUrl(inputContentInfo.getContentUri().toString());
+        mWebView.setBackgroundColor(Color.TRANSPARENT);
+
+        // Due to the asynchronous nature of WebView, it is a bit too early to call
+        // inputContentInfo.releasePermission() here. Hence we call IC#releasePermission() when this
+        // method is called next time.  Note that calling IC#releasePermission() is just to be a
+        // good citizen. Even if we failed to call that method, the system would eventually revoke
+        // the permission sometime after inputContentInfo object gets garbage-collected.
+        mCurrentInputContentInfo = inputContentInfo;
+        mCurrentFlags = flags;
+
+        return true;
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        if (mCurrentInputContentInfo != null) {
+            savedInstanceState.putParcelable(INPUT_CONTENT_INFO_KEY,
+                    (Parcelable) mCurrentInputContentInfo.unwrap());
+            savedInstanceState.putInt(COMMIT_CONTENT_FLAGS_KEY, mCurrentFlags);
+        }
+        mCurrentInputContentInfo = null;
+        mCurrentFlags = 0;
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    /**
+     * Creates a new instance of {@link EditText} that is configured to specify the given content
+     * MIME types to {@link EditorInfo#contentMimeTypes} so that developers
+     * can locally test how the current input method behaves for such content MIME types.
+     *
+     * @param contentMimeTypes A {@link String} array that indicates the supported content MIME
+     *                         types
+     * @return a new instance of {@link EditText}, which specifies
+     * {@link EditorInfo#contentMimeTypes} with the given content
+     * MIME types
+     */
+    private EditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
+        final CharSequence hintText;
+        final String[] mimeTypes;  // our own copy of contentMimeTypes.
+        if (contentMimeTypes == null || contentMimeTypes.length == 0) {
+            hintText = "MIME: []";
+            mimeTypes = new String[0];
+        } else {
+            hintText = "MIME: " + Arrays.toString(contentMimeTypes);
+            mimeTypes = Arrays.copyOf(contentMimeTypes, contentMimeTypes.length);
+        }
+        EditText exitText = new EditText(this) {
+            @Override
+            public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+                final InputConnection ic = super.onCreateInputConnection(editorInfo);
+                EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
+                final InputConnectionCompat.OnCommitContentListener callback =
+                        new InputConnectionCompat.OnCommitContentListener() {
+                            @Override
+                            public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
+                                    int flags, Bundle opts) {
+                                return CommitContentSupport.this.onCommitContent(
+                                        inputContentInfo, flags, opts, mimeTypes);
+                            }
+                        };
+                return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
+            }
+        };
+        exitText.setHint(hintText);
+        exitText.setTextColor(Color.WHITE);
+        exitText.setHintTextColor(Color.WHITE);
+        return exitText;
+    }
+
+    /**
+     * Converts {@code flags} specified in {@link InputConnectionCompat#commitContent(
+     *InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)} to a human readable
+     * string.
+     *
+     * @param flags the 2nd parameter of
+     *              {@link InputConnectionCompat#commitContent(InputConnection, EditorInfo,
+     *              InputContentInfoCompat, int, Bundle)}
+     * @return a human readable string that corresponds to the given {@code flags}
+     */
+    private static String flagsToString(int flags) {
+        final ArrayList<String> tokens = new ArrayList<>();
+        if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
+            tokens.add("INPUT_CONTENT_GRANT_READ_URI_PERMISSION");
+            flags &= ~InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
+        }
+        if (flags != 0) {
+            tokens.add("0x" + Integer.toHexString(flags));
+        }
+        return TextUtils.join(" | ", tokens);
+    }
+
+}