/*
* 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 androidx.core.view.inputmethod;
import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ContentProvider;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.ResultReceiver;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputBinding;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import android.view.inputmethod.InputContentInfo;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import androidx.core.view.ContentInfoCompat;
import androidx.core.view.OnReceiveContentListener;
import androidx.core.view.ViewCompat;
/**
* Helper for accessing features in {@link InputConnection} introduced after API level 13 in a
* backwards compatible fashion.
*/
@SuppressLint("PrivateConstructorForUtilityClass") // Already launched with public constructor
public final class InputConnectionCompat {
private static final String LOG_TAG = "InputConnectionCompat";
private static final String COMMIT_CONTENT_ACTION =
"androidx.core.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT";
private static final String COMMIT_CONTENT_INTEROP_ACTION =
"android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT";
private static final String COMMIT_CONTENT_CONTENT_URI_KEY =
"androidx.core.view.inputmethod.InputConnectionCompat.CONTENT_URI";
private static final String COMMIT_CONTENT_CONTENT_URI_INTEROP_KEY =
"android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI";
private static final String COMMIT_CONTENT_DESCRIPTION_KEY =
"androidx.core.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION";
private static final String COMMIT_CONTENT_DESCRIPTION_INTEROP_KEY =
"android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION";
private static final String COMMIT_CONTENT_LINK_URI_KEY =
"androidx.core.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI";
private static final String COMMIT_CONTENT_LINK_URI_INTEROP_KEY =
"android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI";
private static final String COMMIT_CONTENT_OPTS_KEY =
"androidx.core.view.inputmethod.InputConnectionCompat.CONTENT_OPTS";
private static final String COMMIT_CONTENT_OPTS_INTEROP_KEY =
"android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS";
private static final String COMMIT_CONTENT_FLAGS_KEY =
"androidx.core.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS";
private static final String COMMIT_CONTENT_FLAGS_INTEROP_KEY =
"android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS";
private static final String COMMIT_CONTENT_RESULT_RECEIVER_KEY =
"androidx.core.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER";
private static final String COMMIT_CONTENT_RESULT_INTEROP_RECEIVER_KEY =
"android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER";
@SuppressWarnings("deprecation")
static boolean handlePerformPrivateCommand(
@Nullable String action,
@Nullable Bundle data,
@NonNull OnCommitContentListener onCommitContentListener) {
if (data == null) {
return false;
}
final boolean interop;
if (TextUtils.equals(COMMIT_CONTENT_ACTION, action)) {
interop = false;
} else if (TextUtils.equals(COMMIT_CONTENT_INTEROP_ACTION, action)) {
interop = true;
} else {
return false;
}
ResultReceiver resultReceiver = null;
boolean result = false;
try {
resultReceiver = data.getParcelable(interop
? COMMIT_CONTENT_RESULT_INTEROP_RECEIVER_KEY
: COMMIT_CONTENT_RESULT_RECEIVER_KEY);
final Uri contentUri = data.getParcelable(interop
? COMMIT_CONTENT_CONTENT_URI_INTEROP_KEY
: COMMIT_CONTENT_CONTENT_URI_KEY);
final ClipDescription description = data.getParcelable(interop
? COMMIT_CONTENT_DESCRIPTION_INTEROP_KEY
: COMMIT_CONTENT_DESCRIPTION_KEY);
final Uri linkUri = data.getParcelable(interop
? COMMIT_CONTENT_LINK_URI_INTEROP_KEY
: COMMIT_CONTENT_LINK_URI_KEY);
final int flags = data.getInt(interop
? COMMIT_CONTENT_FLAGS_INTEROP_KEY
: COMMIT_CONTENT_FLAGS_KEY);
final Bundle opts = data.getParcelable(interop
? COMMIT_CONTENT_OPTS_INTEROP_KEY
: COMMIT_CONTENT_OPTS_KEY);
if (contentUri != null && description != null) {
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;
}
/**
* 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) {
if (Build.VERSION.SDK_INT >= 25) {
return Api25Impl.commitContent(inputConnection,
(InputContentInfo) inputContentInfo.unwrap(), flags, opts);
} else {
final int protocol = EditorInfoCompat.getProtocol(editorInfo);
final boolean interop;
switch (protocol) {
case EditorInfoCompat.Protocol.AndroidX_1_0_0:
case EditorInfoCompat.Protocol.AndroidX_1_1_0:
interop = false;
break;
case EditorInfoCompat.Protocol.SupportLib:
interop = true;
break;
default:
// Must not reach here.
return false;
}
final Bundle params = new Bundle();
params.putParcelable(interop
? COMMIT_CONTENT_CONTENT_URI_INTEROP_KEY
: COMMIT_CONTENT_CONTENT_URI_KEY,
inputContentInfo.getContentUri());
params.putParcelable(interop
? COMMIT_CONTENT_DESCRIPTION_INTEROP_KEY
: COMMIT_CONTENT_DESCRIPTION_KEY,
inputContentInfo.getDescription());
params.putParcelable(interop
? COMMIT_CONTENT_LINK_URI_INTEROP_KEY
: COMMIT_CONTENT_LINK_URI_KEY,
inputContentInfo.getLinkUri());
params.putInt(interop
? COMMIT_CONTENT_FLAGS_INTEROP_KEY
: COMMIT_CONTENT_FLAGS_KEY,
flags);
params.putParcelable(interop
? COMMIT_CONTENT_OPTS_INTEROP_KEY
: COMMIT_CONTENT_OPTS_KEY,
opts);
// TODO: Support COMMIT_CONTENT_RESULT_RECEIVER_KEY.
return inputConnection.performPrivateCommand(interop
? COMMIT_CONTENT_INTEROP_ACTION
: COMMIT_CONTENT_ACTION,
params);
}
}
/**
* 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 ContentProvider}.
* </p>
* <ul>
* <li>On API >= 23 {@link EditorInfo#packageName} is guaranteed to not be spoofed, which
* can later be compared with {@link ContentProvider#getCallingPackage()} in
* the {@link ContentProvider}.
* </li>
* <li>{@link InputBinding#getUid()} is guaranteed to not be
* spoofed, which can later be compared with {@link Binder#getCallingUid()} in
* the {@link ContentProvider}.</li>
* </ul>
*/
public static final 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(@NonNull InputContentInfoCompat inputContentInfo, int flags,
@Nullable 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}
*
* @deprecated Use {@link #createWrapper(View,InputConnection,EditorInfo) and
* {@link ViewCompat#setOnReceiveContentListener} instead.
*/
@Deprecated
@NonNull
public static InputConnection createWrapper(@NonNull InputConnection inputConnection,
@NonNull EditorInfo editorInfo,
@NonNull OnCommitContentListener onCommitContentListener) {
ObjectsCompat.requireNonNull(inputConnection, "inputConnection must be non-null");
ObjectsCompat.requireNonNull(editorInfo, "editorInfo must be non-null");
ObjectsCompat.requireNonNull(onCommitContentListener,
"onCommitContentListener must be non-null");
if (Build.VERSION.SDK_INT >= 25) {
final OnCommitContentListener listener = onCommitContentListener;
return new InputConnectionWrapper(inputConnection, false /* mutable */) {
@SuppressWarnings("ConstantConditions") // Incorrect warning
@Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags,
Bundle opts) {
if (listener.onCommitContent(InputContentInfoCompat.wrap(inputContentInfo),
flags, opts)) {
return true;
}
return super.commitContent(inputContentInfo, flags, opts);
}
};
} else {
String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);
if (contentMimeTypes.length == 0) {
return inputConnection;
}
final OnCommitContentListener listener = onCommitContentListener;
return new InputConnectionWrapper(inputConnection, false /* mutable */) {
@Override
public boolean performPrivateCommand(String action, Bundle data) {
if (InputConnectionCompat.handlePerformPrivateCommand(action, data, listener)) {
return true;
}
return super.performPrivateCommand(action, data);
}
};
}
}
/**
* Creates a wrapper {@link InputConnection} that implements {@code InputConnection}'s
* features on past versions of Android.
*
* <p>Currently, handles {@link InputConnection#commitContent} by dispatching to
* {@link ViewCompat#performReceiveContent}, enabling apps to use
* {@link ViewCompat#setOnReceiveContentListener} to specify handling for content insertion
* from the IME.
*
* <p>Usage:<br>
* <pre class="prettyprint">
* public class MyWidget extends View {
* @Override
* public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
* InputConnection ic = super.onCreateInputConnection(outAttrs);
* if (ic == null) {
* return ic;
* }
* String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
* if (mimeTypes != null) {
* EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
* ic = InputConnectionCompat.createWrapper(this, ic, outAttrs);
* }
* return ic;
* }
* }
* </pre>
*
* @param view The view that the given input connection is associated with.
* @param inputConnection The input connection to be wrapped.
* @param editorInfo The editor metadata associated with the given input connection.
*
* @return A wrapper {@link InputConnection} object that can be returned to the IME.
*/
@SuppressWarnings("deprecation")
@NonNull
public static InputConnection createWrapper(@NonNull View view,
@NonNull InputConnection inputConnection, @NonNull EditorInfo editorInfo) {
OnCommitContentListener onCommitContentListener =
createOnCommitContentListenerUsingPerformReceiveContent(view);
return createWrapper(inputConnection, editorInfo, onCommitContentListener);
}
/**
* Creates an {@link OnCommitContentListener} that uses
* {@link ViewCompat#performReceiveContent} to insert content. This is useful for widgets
* that support content insertion using an {@link OnReceiveContentListener}.
*/
@NonNull
private static OnCommitContentListener createOnCommitContentListenerUsingPerformReceiveContent(
@NonNull View view) {
Preconditions.checkNotNull(view);
return (inputContentInfo, flags, opts) -> {
Bundle extras = opts;
if (Build.VERSION.SDK_INT >= 25
&& (flags & INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
Log.w(LOG_TAG,
"Can't insert content from IME; requestPermission() failed", e);
return false;
}
// Permissions granted above are revoked automatically by the platform when the
// corresponding InputContentInfo object is garbage collected. To prevent
// this from happening prematurely (before the receiving app has had a chance
// to process the content), we set the InputContentInfo object into the
// extras of the payload passed to OnReceiveContentListener.
Parcelable inputContentInfoFmk =
(Parcelable) inputContentInfo.unwrap();
extras = (opts == null) ? new Bundle() : new Bundle(opts);
extras.putParcelable(EXTRA_INPUT_CONTENT_INFO, inputContentInfoFmk);
}
ClipData clip = new ClipData(inputContentInfo.getDescription(),
new ClipData.Item(inputContentInfo.getContentUri()));
ContentInfoCompat payload = new ContentInfoCompat.Builder(clip, SOURCE_INPUT_METHOD)
.setLinkUri(inputContentInfo.getLinkUri())
.setExtras(extras)
.build();
return ViewCompat.performReceiveContent(view, payload) == null;
};
}
/**
* Key for extras in {@link ContentInfoCompat}, to hold the {@link InputContentInfo} object
* passed by the IME. Apps should not access/read this object; it is only set in the extras
* in order to prevent premature garbage collection of {@link InputContentInfo} which in
* turn causes premature revocation of URI permissions.
*/
private static final String EXTRA_INPUT_CONTENT_INFO =
"androidx.core.view.extra.INPUT_CONTENT_INFO";
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
public InputConnectionCompat() {
}
@RequiresApi(25)
static class Api25Impl {
private Api25Impl() {
// This class is not instantiable.
}
@DoNotInline
static boolean commitContent(InputConnection inputConnection,
InputContentInfo inputContentInfo, int i, Bundle bundle) {
return inputConnection.commitContent(inputContentInfo, i, bundle);
}
}
}