1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.example.android.supportv13.view.inputmethod;
18
19import com.example.android.supportv13.R;
20
21import android.support.v13.view.inputmethod.EditorInfoCompat;
22import android.support.v13.view.inputmethod.InputConnectionCompat;
23import android.support.v13.view.inputmethod.InputContentInfoCompat;
24
25import android.app.Activity;
26import android.graphics.Color;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Parcelable;
30import android.text.TextUtils;
31import android.util.Log;
32import android.view.inputmethod.EditorInfo;
33import android.view.inputmethod.InputConnection;
34import android.webkit.WebView;
35import android.widget.EditText;
36import android.widget.LinearLayout;
37import android.widget.TextView;
38
39import java.util.ArrayList;
40import java.util.Arrays;
41
42public class CommitContentSupport extends Activity {
43    private static final String INPUT_CONTENT_INFO_KEY = "COMMIT_CONTENT_INPUT_CONTENT_INFO";
44    private static final String COMMIT_CONTENT_FLAGS_KEY = "COMMIT_CONTENT_FLAGS";
45
46    private static String TAG = "CommitContentSupport";
47
48    private WebView mWebView;
49    private TextView mLabel;
50    private TextView mContentUri;
51    private TextView mLinkUri;
52    private TextView mMimeTypes;
53    private TextView mFlags;
54
55    private InputContentInfoCompat mCurrentInputContentInfo;
56    private int mCurrentFlags;
57
58    @Override
59    public void onCreate(Bundle savedInstanceState) {
60        super.onCreate(savedInstanceState);
61
62        setContentView(R.layout.commit_content);
63
64        final LinearLayout layout =
65                findViewById(R.id.commit_content_sample_edit_boxes);
66
67        // This declares that the IME cannot commit any content with
68        // InputConnectionCompat#commitContent().
69        layout.addView(createEditTextWithContentMimeTypes(null));
70
71        // This declares that the IME can commit contents with
72        // InputConnectionCompat#commitContent() if they match "image/gif".
73        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/gif"}));
74
75        // This declares that the IME can commit contents with
76        // InputConnectionCompat#commitContent() if they match "image/png".
77        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/png"}));
78
79        // This declares that the IME can commit contents with
80        // InputConnectionCompat#commitContent() if they match "image/jpeg".
81        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/jpeg"}));
82
83        // This declares that the IME can commit contents with
84        // InputConnectionCompat#commitContent() if they match "image/webp".
85        layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/webp"}));
86
87        // This declares that the IME can commit contents with
88        // InputConnectionCompat#commitContent() if they match "image/png", "image/gif",
89        // "image/jpeg", or "image/webp".
90        layout.addView(createEditTextWithContentMimeTypes(
91                new String[]{"image/png", "image/gif", "image/jpeg", "image/webp"}));
92
93        mWebView = findViewById(R.id.commit_content_webview);
94        mMimeTypes = findViewById(R.id.text_commit_content_mime_types);
95        mLabel = findViewById(R.id.text_commit_content_label);
96        mContentUri = findViewById(R.id.text_commit_content_content_uri);
97        mLinkUri = findViewById(R.id.text_commit_content_link_uri);
98        mFlags = findViewById(R.id.text_commit_content_link_flags);
99
100        if (savedInstanceState != null) {
101            final InputContentInfoCompat previousInputContentInfo = InputContentInfoCompat.wrap(
102                    savedInstanceState.getParcelable(INPUT_CONTENT_INFO_KEY));
103            final int previousFlags = savedInstanceState.getInt(COMMIT_CONTENT_FLAGS_KEY);
104            if (previousInputContentInfo != null) {
105                onCommitContentInternal(previousInputContentInfo, previousFlags);
106            }
107        }
108    }
109
110    private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
111            Bundle opts, String[] contentMimeTypes) {
112        // Clear the temporary permission (if any).  See below about why we do this here.
113        try {
114            if (mCurrentInputContentInfo != null) {
115                mCurrentInputContentInfo.releasePermission();
116            }
117        } catch (Exception e) {
118            Log.e(TAG, "InputContentInfoCompat#releasePermission() failed.", e);
119        } finally {
120            mCurrentInputContentInfo = null;
121        }
122
123        mWebView.loadUrl("about:blank");
124        mMimeTypes.setText("");
125        mContentUri.setText("");
126        mLabel.setText("");
127        mLinkUri.setText("");
128        mFlags.setText("");
129
130        boolean supported = false;
131        for (final String mimeType : contentMimeTypes) {
132            if (inputContentInfo.getDescription().hasMimeType(mimeType)) {
133                supported = true;
134                break;
135            }
136        }
137        if (!supported) {
138            return false;
139        }
140
141        return onCommitContentInternal(inputContentInfo, flags);
142    }
143
144    private boolean onCommitContentInternal(InputContentInfoCompat inputContentInfo, int flags) {
145        if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
146            try {
147                inputContentInfo.requestPermission();
148            } catch (Exception e) {
149                Log.e(TAG, "InputContentInfoCompat#requestPermission() failed.", e);
150                return false;
151            }
152        }
153
154        mMimeTypes.setText(
155                Arrays.toString(inputContentInfo.getDescription().filterMimeTypes("*/*")));
156        mContentUri.setText(inputContentInfo.getContentUri().toString());
157        mLabel.setText(inputContentInfo.getDescription().getLabel());
158        Uri linkUri = inputContentInfo.getLinkUri();
159        mLinkUri.setText(linkUri != null ? linkUri.toString() : "null");
160        mFlags.setText(flagsToString(flags));
161        mWebView.loadUrl(inputContentInfo.getContentUri().toString());
162        mWebView.setBackgroundColor(Color.TRANSPARENT);
163
164        // Due to the asynchronous nature of WebView, it is a bit too early to call
165        // inputContentInfo.releasePermission() here. Hence we call IC#releasePermission() when this
166        // method is called next time.  Note that calling IC#releasePermission() is just to be a
167        // good citizen. Even if we failed to call that method, the system would eventually revoke
168        // the permission sometime after inputContentInfo object gets garbage-collected.
169        mCurrentInputContentInfo = inputContentInfo;
170        mCurrentFlags = flags;
171
172        return true;
173    }
174
175    @Override
176    public void onSaveInstanceState(Bundle savedInstanceState) {
177        if (mCurrentInputContentInfo != null) {
178            savedInstanceState.putParcelable(INPUT_CONTENT_INFO_KEY,
179                    (Parcelable) mCurrentInputContentInfo.unwrap());
180            savedInstanceState.putInt(COMMIT_CONTENT_FLAGS_KEY, mCurrentFlags);
181        }
182        mCurrentInputContentInfo = null;
183        mCurrentFlags = 0;
184        super.onSaveInstanceState(savedInstanceState);
185    }
186
187    /**
188     * Creates a new instance of {@link EditText} that is configured to specify the given content
189     * MIME types to {@link EditorInfo#contentMimeTypes} so that developers
190     * can locally test how the current input method behaves for such content MIME types.
191     *
192     * @param contentMimeTypes A {@link String} array that indicates the supported content MIME
193     *                         types
194     * @return a new instance of {@link EditText}, which specifies
195     * {@link EditorInfo#contentMimeTypes} with the given content
196     * MIME types
197     */
198    private EditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
199        final CharSequence hintText;
200        final String[] mimeTypes;  // our own copy of contentMimeTypes.
201        if (contentMimeTypes == null || contentMimeTypes.length == 0) {
202            hintText = "MIME: []";
203            mimeTypes = new String[0];
204        } else {
205            hintText = "MIME: " + Arrays.toString(contentMimeTypes);
206            mimeTypes = Arrays.copyOf(contentMimeTypes, contentMimeTypes.length);
207        }
208        EditText exitText = new EditText(this) {
209            @Override
210            public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
211                final InputConnection ic = super.onCreateInputConnection(editorInfo);
212                EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
213                final InputConnectionCompat.OnCommitContentListener callback =
214                        new InputConnectionCompat.OnCommitContentListener() {
215                            @Override
216                            public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
217                                    int flags, Bundle opts) {
218                                return CommitContentSupport.this.onCommitContent(
219                                        inputContentInfo, flags, opts, mimeTypes);
220                            }
221                        };
222                return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
223            }
224        };
225        exitText.setHint(hintText);
226        exitText.setTextColor(Color.WHITE);
227        exitText.setHintTextColor(Color.WHITE);
228        return exitText;
229    }
230
231    /**
232     * Converts {@code flags} specified in {@link InputConnectionCompat#commitContent(
233     *InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)} to a human readable
234     * string.
235     *
236     * @param flags the 2nd parameter of
237     *              {@link InputConnectionCompat#commitContent(InputConnection, EditorInfo,
238     *              InputContentInfoCompat, int, Bundle)}
239     * @return a human readable string that corresponds to the given {@code flags}
240     */
241    private static String flagsToString(int flags) {
242        final ArrayList<String> tokens = new ArrayList<>();
243        if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
244            tokens.add("INPUT_CONTENT_GRANT_READ_URI_PERMISSION");
245            flags &= ~InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
246        }
247        if (flags != 0) {
248            tokens.add("0x" + Integer.toHexString(flags));
249        }
250        return TextUtils.join(" | ", tokens);
251    }
252
253}
254