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