/** * 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 android.support.v13.view.inputmethod; import android.annotation.TargetApi; import android.content.ClipDescription; import android.net.Uri; import android.os.Bundle; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.os.BuildCompat; import android.text.TextUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionWrapper; /** * Helper for accessing features in {@link InputConnection} introduced after API level 13 in a * backwards compatible fashion. */ @RequiresApi(13) @TargetApi(13) public final class InputConnectionCompat { private interface InputConnectionCompatImpl { boolean commitContent(@NonNull InputConnection inputConnection, @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts); @NonNull InputConnection createWrapper(@NonNull InputConnection ic, @NonNull EditorInfo editorInfo, @NonNull OnCommitContentListener callback); } static final class BaseInputContentInfoCompatImpl implements InputConnectionCompatImpl { private static String COMMIT_CONTENT_ACTION = "android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT"; private static String COMMIT_CONTENT_CONTENT_URI_KEY = "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI"; private static String COMMIT_CONTENT_DESCRIPTION_KEY = "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION"; private static String COMMIT_CONTENT_LINK_URI_KEY = "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI"; private static String COMMIT_CONTENT_OPTS_KEY = "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS"; private static String COMMIT_CONTENT_FLAGS_KEY = "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS"; private static String COMMIT_CONTENT_RESULT_RECEIVER = "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER"; @Override public boolean commitContent(@NonNull InputConnection inputConnection, @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts) { final Bundle params = new Bundle(); params.putParcelable(COMMIT_CONTENT_CONTENT_URI_KEY, inputContentInfo.getContentUri()); params.putParcelable(COMMIT_CONTENT_DESCRIPTION_KEY, inputContentInfo.getDescription()); params.putParcelable(COMMIT_CONTENT_LINK_URI_KEY, inputContentInfo.getLinkUri()); params.putInt(COMMIT_CONTENT_FLAGS_KEY, flags); params.putParcelable(COMMIT_CONTENT_OPTS_KEY, opts); // TODO: Support COMMIT_CONTENT_RESULT_RECEIVER. return inputConnection.performPrivateCommand(COMMIT_CONTENT_ACTION, params); } @NonNull @Override public InputConnection createWrapper(@NonNull InputConnection ic, @NonNull EditorInfo editorInfo, @NonNull OnCommitContentListener onCommitContentListener) { String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo); if (contentMimeTypes.length == 0) { return ic; } final OnCommitContentListener listener = onCommitContentListener; return new InputConnectionWrapper(ic, false /* mutable */) { @Override public boolean performPrivateCommand(String action, Bundle data) { if (BaseInputContentInfoCompatImpl.handlePerformPrivateCommand(action, data, listener)) { return true; } return super.performPrivateCommand(action, data); } }; } static boolean handlePerformPrivateCommand( @Nullable String action, @NonNull Bundle data, @NonNull OnCommitContentListener onCommitContentListener) { if (!TextUtils.equals(COMMIT_CONTENT_ACTION, action)) { return false; } if (data == null) { return false; } ResultReceiver resultReceiver = null; boolean result = false; try { resultReceiver = data.getParcelable(COMMIT_CONTENT_RESULT_RECEIVER); final Uri contentUri = data.getParcelable(COMMIT_CONTENT_CONTENT_URI_KEY); final ClipDescription description = data.getParcelable( COMMIT_CONTENT_DESCRIPTION_KEY); final Uri linkUri = data.getParcelable(COMMIT_CONTENT_LINK_URI_KEY); final int flags = data.getInt(COMMIT_CONTENT_FLAGS_KEY); final Bundle opts = data.getParcelable(COMMIT_CONTENT_OPTS_KEY); final InputContentInfoCompat inputContentInfo = new InputContentInfoCompat(contentUri, description, linkUri); result = onCommitContentListener.onCommitContent(inputContentInfo, flags, opts); } finally { if (resultReceiver != null) { resultReceiver.send(result ? 1 : 0, null); } } return result; } } private final static class Api25InputContentInfoCompatImpl implements InputConnectionCompatImpl { @Override public boolean commitContent(@NonNull InputConnection inputConnection, @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts) { return InputConnectionCompatApi25.commitContent(inputConnection, inputContentInfo.unwrap(), flags, opts); } @Nullable @Override public InputConnection createWrapper( @Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, @Nullable OnCommitContentListener onCommitContentListener) { final OnCommitContentListener listener = onCommitContentListener; return InputConnectionCompatApi25.createWrapper( inputConnection, new InputConnectionCompatApi25.OnCommitContentListener() { @Override public boolean onCommitContent(Object inputContentInfo, int flags, Bundle opts) { InputContentInfoCompat inputContentInfoCompat = InputContentInfoCompat.wrap(inputContentInfo); return listener.onCommitContent(inputContentInfoCompat, flags, opts); } }); } } private static final InputConnectionCompatImpl IMPL; static { if (BuildCompat.isAtLeastNMR1()) { IMPL = new Api25InputContentInfoCompatImpl(); } else { IMPL = new BaseInputContentInfoCompatImpl(); } } /** * Calls commitContent API, in a backwards compatible fashion. * * @param inputConnection {@link InputConnection} with which commitContent API will be called * @param editorInfo {@link EditorInfo} associated with the given {@code inputConnection} * @param inputContentInfo content information to be passed to the editor * @param flags {@code 0} or {@link #INPUT_CONTENT_GRANT_READ_URI_PERMISSION} * @param opts optional bundle data. This can be {@code null} * @return {@code true} if this request is accepted by the application, no matter if the request * is already handled or still being handled in background */ public static boolean commitContent(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts) { final ClipDescription description = inputContentInfo.getDescription(); boolean supported = false; for (String mimeType : EditorInfoCompat.getContentMimeTypes(editorInfo)) { if (description.hasMimeType(mimeType)) { supported = true; break; } } if (!supported) { return false; } return IMPL.commitContent(inputConnection, inputContentInfo, flags, opts); } /** * When this flag is used, the editor will be able to request temporary access permissions to * the content URI contained in the {@link InputContentInfoCompat} object, in a similar manner * that has been recommended in * <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files</a>. * * <p>Make sure that the content provider owning the Uri sets the * {@link android.R.attr#grantUriPermissions grantUriPermissions} attribute in its manifest or * included the {@code <grant-uri-permissions>} tag.</p> * * <p>Supported only on API >= 25.</p> * * <p>On API <= 24 devices, IME developers need to ensure that the content URI is accessible * only from the target application, for example, by generating a URL with a unique name that * others cannot guess. IME developers can also rely on the following information of the target * application to do additional access checks in their {@link android.content.ContentProvider}. * </p> * <ul> * <li>On API >= 23 {@link EditorInfo#packageName} is guaranteed to not be spoofed, which * can later be compared with {@link android.content.ContentProvider#getCallingPackage()} in * the {@link android.content.ContentProvider}. * </li> * <li>{@link android.view.inputmethod.InputBinding#getUid()} is guaranteed to not be * spoofed, which can later be compared with {@link android.os.Binder#getCallingUid()} in * the {@link android.content.ContentProvider}.</li> * </ul> */ public static int INPUT_CONTENT_GRANT_READ_URI_PERMISSION = 0x00000001; /** * Listener for commitContent method call, in a backwards compatible fashion. */ public interface OnCommitContentListener { /** * Intercepts InputConnection#commitContent API calls. * * @param inputContentInfo content to be committed * @param flags {@code 0} or {@link #INPUT_CONTENT_GRANT_READ_URI_PERMISSION} * @param opts optional bundle data. This can be {@code null} * @return {@code true} if this request is accepted by the application, no matter if the * request is already handled or still being handled in background. {@code false} to use the * default implementation */ boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts); } /** * Creates a wrapper {@link InputConnection} object from an existing {@link InputConnection} * and {@link OnCommitContentListener} that can be returned to the system. * * <p>By returning the wrapper object to the IME, the editor can be notified by * {@link OnCommitContentListener#onCommitContent(InputContentInfoCompat, int, Bundle)} * when the IME calls * {@link InputConnectionCompat#commitContent(InputConnection, EditorInfo, * InputContentInfoCompat, int, Bundle)} and the corresponding Framework API that is available * on API >= 25.</p> * * @param inputConnection {@link InputConnection} to be wrapped * @param editorInfo {@link EditorInfo} associated with the given {@code inputConnection} * @param onCommitContentListener the listener that the wrapper object will call * @return a wrapper {@link InputConnection} object that can be returned to the IME * @throws IllegalArgumentException when {@code inputConnection}, {@code editorInfo}, or * {@code onCommitContentListener} is {@code null} */ @NonNull public static InputConnection createWrapper(@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo, @NonNull OnCommitContentListener onCommitContentListener) { if (inputConnection == null) { throw new IllegalArgumentException("inputConnection must be non-null"); } if (editorInfo == null) { throw new IllegalArgumentException("editorInfo must be non-null"); } if (onCommitContentListener == null) { throw new IllegalArgumentException("onCommitContentListener must be non-null"); } return IMPL.createWrapper(inputConnection, editorInfo, onCommitContentListener); } }