From 152944f4909c47917473293b258d266435c6ab35 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Fri, 10 Jun 2016 19:04:34 -0700 Subject: [PATCH] Add InputConnection#insertContent(). Providing an official protocol for IMEs to insert an image to the application is something that has been requested from many IME developers to Android OS. With this CL, IMEs are able to ask applications to insert a content including image files as follows. 1. An application that opts in to this protocol specifies a list of supported content MIME types in EditorInfo#contentMimeTypes. 2. When an IME is actively interacting with such an application, the IME can call InputConnection#insertContent() with a InputContentInfo that contains content URI, metadata (ClipDescription), and an optional link URI. 3. The application can read the stream data from the given content URI to insert the content into somewhere in the application. Detailed design background can be found in the JavaDoc of InputConnection#insertContent(). Bug: 22830793 Change-Id: Iaadf934a997ffcd6000a516cc3c1873db56e60ad --- api/current.txt | 15 ++ api/system-current.txt | 15 ++ api/test-current.txt | 15 ++ .../view/inputmethod/BaseInputConnection.java | 5 + .../android/view/inputmethod/EditorInfo.java | 17 ++ .../view/inputmethod/InputConnection.java | 31 +++ .../inputmethod/InputConnectionInspector.java | 27 +++ .../inputmethod/InputConnectionWrapper.java | 8 + .../view/inputmethod/InputContentInfo.aidl | 19 ++ .../view/inputmethod/InputContentInfo.java | 194 ++++++++++++++++++ core/java/android/widget/AbsListView.java | 7 + .../view/IInputConnectionWrapper.java | 34 ++- .../android/internal/view/IInputContext.aidl | 6 +- .../internal/view/IInputContextCallback.aidl | 1 + .../internal/view/InputConnectionWrapper.java | 40 +++- 15 files changed, 431 insertions(+), 3 deletions(-) create mode 100644 core/java/android/view/inputmethod/InputContentInfo.aidl create mode 100644 core/java/android/view/inputmethod/InputContentInfo.java diff --git a/api/current.txt b/api/current.txt index 64eff6b5daf55..651b17612443d 100644 --- a/api/current.txt +++ b/api/current.txt @@ -44715,6 +44715,7 @@ package android.view.inputmethod { method public java.lang.CharSequence getSelectedText(int); method public java.lang.CharSequence getTextAfterCursor(int, int); method public java.lang.CharSequence getTextBeforeCursor(int, int); + method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public boolean performContextMenuAction(int); method public boolean performEditorAction(int); method public boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -44809,6 +44810,7 @@ package android.view.inputmethod { field public static final int IME_NULL = 0; // 0x0 field public int actionId; field public java.lang.CharSequence actionLabel; + field public java.lang.String[] contentMimeTypes; field public android.os.Bundle extras; field public int fieldId; field public java.lang.String fieldName; @@ -44880,6 +44882,7 @@ package android.view.inputmethod { method public abstract java.lang.CharSequence getSelectedText(int); method public abstract java.lang.CharSequence getTextAfterCursor(int, int); method public abstract java.lang.CharSequence getTextBeforeCursor(int, int); + method public abstract boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public abstract boolean performContextMenuAction(int); method public abstract boolean performEditorAction(int); method public abstract boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -44913,6 +44916,7 @@ package android.view.inputmethod { method public java.lang.CharSequence getSelectedText(int); method public java.lang.CharSequence getTextAfterCursor(int, int); method public java.lang.CharSequence getTextBeforeCursor(int, int); + method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public boolean performContextMenuAction(int); method public boolean performEditorAction(int); method public boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -44925,6 +44929,17 @@ package android.view.inputmethod { method public void setTarget(android.view.inputmethod.InputConnection); } + public class InputContentInfo implements android.os.Parcelable { + ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription); + ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri); + method public int describeContents(); + method public android.net.Uri getContentUri(); + method public android.content.ClipDescription getDescription(); + method public android.net.Uri getLinkUri(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + public abstract interface InputMethod { method public abstract void attachToken(android.os.IBinder); method public abstract void bindInput(android.view.inputmethod.InputBinding); diff --git a/api/system-current.txt b/api/system-current.txt index bdf8299ba54eb..8c3d5b15c37ae 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -47716,6 +47716,7 @@ package android.view.inputmethod { method public java.lang.CharSequence getSelectedText(int); method public java.lang.CharSequence getTextAfterCursor(int, int); method public java.lang.CharSequence getTextBeforeCursor(int, int); + method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public boolean performContextMenuAction(int); method public boolean performEditorAction(int); method public boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -47810,6 +47811,7 @@ package android.view.inputmethod { field public static final int IME_NULL = 0; // 0x0 field public int actionId; field public java.lang.CharSequence actionLabel; + field public java.lang.String[] contentMimeTypes; field public android.os.Bundle extras; field public int fieldId; field public java.lang.String fieldName; @@ -47881,6 +47883,7 @@ package android.view.inputmethod { method public abstract java.lang.CharSequence getSelectedText(int); method public abstract java.lang.CharSequence getTextAfterCursor(int, int); method public abstract java.lang.CharSequence getTextBeforeCursor(int, int); + method public abstract boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public abstract boolean performContextMenuAction(int); method public abstract boolean performEditorAction(int); method public abstract boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -47914,6 +47917,7 @@ package android.view.inputmethod { method public java.lang.CharSequence getSelectedText(int); method public java.lang.CharSequence getTextAfterCursor(int, int); method public java.lang.CharSequence getTextBeforeCursor(int, int); + method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public boolean performContextMenuAction(int); method public boolean performEditorAction(int); method public boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -47926,6 +47930,17 @@ package android.view.inputmethod { method public void setTarget(android.view.inputmethod.InputConnection); } + public class InputContentInfo implements android.os.Parcelable { + ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription); + ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri); + method public int describeContents(); + method public android.net.Uri getContentUri(); + method public android.content.ClipDescription getDescription(); + method public android.net.Uri getLinkUri(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + public abstract interface InputMethod { method public abstract void attachToken(android.os.IBinder); method public abstract void bindInput(android.view.inputmethod.InputBinding); diff --git a/api/test-current.txt b/api/test-current.txt index 93b73686ef64a..41a6d422a016f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -44795,6 +44795,7 @@ package android.view.inputmethod { method public java.lang.CharSequence getSelectedText(int); method public java.lang.CharSequence getTextAfterCursor(int, int); method public java.lang.CharSequence getTextBeforeCursor(int, int); + method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public boolean performContextMenuAction(int); method public boolean performEditorAction(int); method public boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -44889,6 +44890,7 @@ package android.view.inputmethod { field public static final int IME_NULL = 0; // 0x0 field public int actionId; field public java.lang.CharSequence actionLabel; + field public java.lang.String[] contentMimeTypes; field public android.os.Bundle extras; field public int fieldId; field public java.lang.String fieldName; @@ -44960,6 +44962,7 @@ package android.view.inputmethod { method public abstract java.lang.CharSequence getSelectedText(int); method public abstract java.lang.CharSequence getTextAfterCursor(int, int); method public abstract java.lang.CharSequence getTextBeforeCursor(int, int); + method public abstract boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public abstract boolean performContextMenuAction(int); method public abstract boolean performEditorAction(int); method public abstract boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -44993,6 +44996,7 @@ package android.view.inputmethod { method public java.lang.CharSequence getSelectedText(int); method public java.lang.CharSequence getTextAfterCursor(int, int); method public java.lang.CharSequence getTextBeforeCursor(int, int); + method public boolean insertContent(android.view.inputmethod.InputContentInfo, android.os.Bundle); method public boolean performContextMenuAction(int); method public boolean performEditorAction(int); method public boolean performPrivateCommand(java.lang.String, android.os.Bundle); @@ -45005,6 +45009,17 @@ package android.view.inputmethod { method public void setTarget(android.view.inputmethod.InputConnection); } + public class InputContentInfo implements android.os.Parcelable { + ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription); + ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri); + method public int describeContents(); + method public android.net.Uri getContentUri(); + method public android.content.ClipDescription getDescription(); + method public android.net.Uri getLinkUri(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + public abstract interface InputMethod { method public abstract void attachToken(android.os.IBinder); method public abstract void bindInput(android.view.inputmethod.InputBinding); diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 89dec2d22d7a7..7c3fc8b5808bd 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -851,4 +851,9 @@ public class BaseInputConnection implements InputConnection { endBatchEdit(); } + + /** + * The default implementation does nothing. + */ + public boolean insertContent(InputContentInfo inputContentInfo, Bundle opts) { return false; } } diff --git a/core/java/android/view/inputmethod/EditorInfo.java b/core/java/android/view/inputmethod/EditorInfo.java index 7b7ccae62dede..7ed0d27f65ece 100644 --- a/core/java/android/view/inputmethod/EditorInfo.java +++ b/core/java/android/view/inputmethod/EditorInfo.java @@ -25,6 +25,8 @@ import android.text.InputType; import android.text.TextUtils; import android.util.Printer; +import java.util.Arrays; + /** * An EditorInfo describes several attributes of a text editing object * that an input method is communicating with (typically an EditText), most @@ -363,6 +365,18 @@ public class EditorInfo implements InputType, Parcelable { @Nullable public LocaleList hintLocales = null; + + /** + * List of acceptable MIME types for + * {@link InputConnection#insertContent(InputContentInfo, Bundle)}. + * + *

{@code null} or an empty array means that + * {@link InputConnection#insertContent(InputContentInfo, Bundle)} is not supported in this + * editor.

+ */ + @Nullable + public String[] contentMimeTypes = null; + /** * Ensure that the data in this EditorInfo is compatible with an application * that was developed against the given target API version. This can @@ -418,6 +432,7 @@ public class EditorInfo implements InputType, Parcelable { + " fieldName=" + fieldName); pw.println(prefix + "extras=" + extras); pw.println(prefix + "hintLocales=" + hintLocales); + pw.println(prefix + "contentMimeTypes=" + Arrays.toString(contentMimeTypes)); } /** @@ -446,6 +461,7 @@ public class EditorInfo implements InputType, Parcelable { } else { LocaleList.getEmptyLocaleList().writeToParcel(dest, flags); } + dest.writeStringArray(contentMimeTypes); } /** @@ -471,6 +487,7 @@ public class EditorInfo implements InputType, Parcelable { res.extras = source.readBundle(); LocaleList hintLocales = LocaleList.CREATOR.createFromParcel(source); res.hintLocales = hintLocales.isEmpty() ? null : hintLocales; + res.contentMimeTypes = source.readStringArray(); return res; } diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 9f66429347683..05e05699df944 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -16,6 +16,8 @@ package android.view.inputmethod; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Bundle; import android.os.Handler; import android.view.KeyCharacterMap; @@ -836,4 +838,33 @@ public interface InputConnection { *

Note: This does nothing when called from input methods.

*/ public void closeConnection(); + + /** + * Called by the input method to insert a content such as PNG image to the editor. + * + *

In order to avoid variety of compatibility issues, this focuses on a simple use case, + * where we expect editors and IMEs work cooperatively as follows:

+ *
    + *
  • Editor must keep {@link EditorInfo#contentMimeTypes} to be {@code null} if it does + * not support this method at all.
  • + *
  • Editor can ignore this request when the MIME type specified in + * {@code inputContentInfo} does not match to any of {@link EditorInfo#contentMimeTypes}. + *
  • + *
  • Editor can ignore the cursor position when inserting the provided context.
  • + *
  • Editor can return {@code true} asynchronously, even before it starts loading the + * content.
  • + *
  • Editor should provide a way to delete the content inserted by this method, or revert + * the effect caused by this method.
  • + *
  • IME should not call this method when there is any composing text, in case calling + * this method causes focus change.
  • + *
  • IME should grant a permission for the editor to read the content. See + * {@link EditorInfo#packageName} about how to obtain the package name of the editor.
  • + *
+ * + * @param inputContentInfo Content to be inserted. + * @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 boolean insertContent(@NonNull InputContentInfo inputContentInfo, @Nullable Bundle opts); } diff --git a/core/java/android/view/inputmethod/InputConnectionInspector.java b/core/java/android/view/inputmethod/InputConnectionInspector.java index 118a61f8d571c..fb24fcd8c77a2 100644 --- a/core/java/android/view/inputmethod/InputConnectionInspector.java +++ b/core/java/android/view/inputmethod/InputConnectionInspector.java @@ -19,6 +19,7 @@ package android.view.inputmethod; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Bundle; import java.lang.annotation.Retention; import java.lang.reflect.Method; @@ -41,6 +42,8 @@ public final class InputConnectionInspector { MissingMethodFlags.REQUEST_CURSOR_UPDATES, MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS, MissingMethodFlags.GET_HANDLER, + MissingMethodFlags.CLOSE_CONNECTION, + MissingMethodFlags.INSERT_CONTENT, }) public @interface MissingMethodFlags { /** @@ -78,6 +81,11 @@ public final class InputConnectionInspector { * {@link android.os.Build.VERSION_CODES#N} and later. */ int CLOSE_CONNECTION = 1 << 6; + /** + * {@link InputConnection#insertContent(InputContentInfo, Bundle)} is available in + * {@link android.os.Build.VERSION_CODES#N} MR-1 and later. + */ + int INSERT_CONTENT = 1 << 7; } private static final Map sMissingMethodsMap = Collections.synchronizedMap( @@ -127,6 +135,9 @@ public final class InputConnectionInspector { if (!hasCloseConnection(clazz)) { flags |= MissingMethodFlags.CLOSE_CONNECTION; } + if (!hasInsertContent(clazz)) { + flags |= MissingMethodFlags.INSERT_CONTENT; + } sMissingMethodsMap.put(clazz, flags); return flags; } @@ -195,6 +206,16 @@ public final class InputConnectionInspector { } } + private static boolean hasInsertContent(@NonNull final Class clazz) { + try { + final Method method = clazz.getMethod("insertContent", InputContentInfo.class, + Bundle.class); + return !Modifier.isAbstract(method.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) { final StringBuilder sb = new StringBuilder(); boolean isEmpty = true; @@ -242,6 +263,12 @@ public final class InputConnectionInspector { } sb.append("closeConnection()"); } + if ((flags & MissingMethodFlags.INSERT_CONTENT) != 0) { + if (!isEmpty) { + sb.append(","); + } + sb.append("InsertContent(InputContentInfo, Bundle)"); + } return sb.toString(); } } diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index e743f62a5a0a6..431836a44f84d 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -269,4 +269,12 @@ public class InputConnectionWrapper implements InputConnection { public void closeConnection() { mTarget.closeConnection(); } + + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + public boolean insertContent(InputContentInfo inputContentInfo, Bundle opts) { + return mTarget.insertContent(inputContentInfo, opts); + } } diff --git a/core/java/android/view/inputmethod/InputContentInfo.aidl b/core/java/android/view/inputmethod/InputContentInfo.aidl new file mode 100644 index 0000000000000..1afeee3b4b245 --- /dev/null +++ b/core/java/android/view/inputmethod/InputContentInfo.aidl @@ -0,0 +1,19 @@ +/* + * 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.view.inputmethod; + +parcelable InputContentInfo; diff --git a/core/java/android/view/inputmethod/InputContentInfo.java b/core/java/android/view/inputmethod/InputContentInfo.java new file mode 100644 index 0000000000000..e2ecfae7f7642 --- /dev/null +++ b/core/java/android/view/inputmethod/InputContentInfo.java @@ -0,0 +1,194 @@ +/* + * 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.view.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ClipDescription; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.security.InvalidParameterException; + +/** + * A container object with which input methods can send content files to the target application. + */ +public class InputContentInfo implements Parcelable { + + @NonNull + private final Uri mContentUri; + @NonNull + private final ClipDescription mDescription; + @Nullable + private final Uri mLinkUri; + + /** + * Constructs {@link InputContentInfo} object only with mandatory data. + * + * @param contentUri Content URI to be exported from the input method. + * This cannot be {@code null}. + * @param description A {@link ClipDescription} object that contains the metadata of + * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also + * {@link ClipDescription#getLabel()} should be describing the content specified by + * {@code contentUri} for accessibility reasons. + */ + public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description) { + this(contentUri, description, null /* link Uri */); + } + + /** + * Constructs {@link InputContentInfo} object with additional link URI. + * + * @param contentUri Content URI to be exported from the input method. + * This cannot be {@code null}. + * @param description A {@link ClipDescription} object that contains the metadata of + * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also + * {@link ClipDescription#getLabel()} should be describing the content specified by + * {@code contentUri} for accessibility reasons. + * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide + * a way to navigate the user to the specified web page if this is not {@code null}. + * @throws InvalidParameterException if any invalid parameter is specified. + */ + public InputContentInfo(@NonNull Uri contentUri, @NonNull ClipDescription description, + @Nullable Uri linkUri) { + validateInternal(contentUri, description, linkUri, true /* throwException */); + mContentUri = contentUri; + mDescription = description; + mLinkUri = linkUri; + } + + /** + * @return {@code true} if all the fields are valid. + * @hide + */ + public boolean validate() { + return validateInternal(mContentUri, mDescription, mLinkUri, false /* throwException */); + } + + /** + * Constructs {@link InputContentInfo} object with additional link URI. + * + * @param contentUri Content URI to be exported from the input method. + * This cannot be {@code null}. + * @param description A {@link ClipDescription} object that contains the metadata of + * {@code contentUri} such as MIME type(s). This object cannot be {@code null}. Also + * {@link ClipDescription#getLabel()} should be describing the content specified by + * {@code contentUri} for accessibility reasons. + * @param linkUri An optional {@code http} or {@code https} URI. The editor author may provide + * a way to navigate the user to the specified web page if this is not {@code null}. + * @param throwException {@code true} if this method should throw an + * {@link InvalidParameterException}. + * @throws InvalidParameterException if any invalid parameter is specified. + */ + private static boolean validateInternal(@NonNull Uri contentUri, + @NonNull ClipDescription description, @Nullable Uri linkUri, boolean throwException) { + if (contentUri == null) { + if (throwException) { + throw new NullPointerException("contentUri"); + } + return false; + } + if (description == null) { + if (throwException) { + throw new NullPointerException("description"); + } + return false; + } + final String contentUriScheme = contentUri.getScheme(); + if (contentUriScheme == null || !contentUriScheme.equalsIgnoreCase("content")) { + if (throwException) { + throw new InvalidParameterException("contentUri must have content scheme"); + } + return false; + } + if (linkUri != null) { + final String scheme = linkUri.getScheme(); + if (scheme == null || + (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) { + if (throwException) { + throw new InvalidParameterException( + "linkUri must have either http or https scheme"); + } + return false; + } + } + return true; + } + + /** + * @return Content URI with which the content can be obtained. + */ + @NonNull + public Uri getContentUri() { return mContentUri; } + + /** + * @return {@link ClipDescription} object that contains the metadata of {@code contentUri} such + * as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility purpose. + */ + @NonNull + public ClipDescription getDescription() { return mDescription; } + + /** + * @return An optional {@code http} or {@code https} URI that is related to this content. + */ + @Nullable + public Uri getLinkUri() { return mLinkUri; } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + Uri.writeToParcel(dest, mContentUri); + mDescription.writeToParcel(dest, flags); + Uri.writeToParcel(dest, mLinkUri); + } + + private InputContentInfo(@NonNull Parcel source) { + mContentUri = Uri.CREATOR.createFromParcel(source); + mDescription = ClipDescription.CREATOR.createFromParcel(source); + mLinkUri = Uri.CREATOR.createFromParcel(source); + } + + /** + * Used to make this class parcelable. + */ + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public InputContentInfo createFromParcel(Parcel source) { + return new InputContentInfo(source); + } + + @Override + public InputContentInfo[] newArray(int size) { + return new InputContentInfo[size]; + } + }; + + /** + * {@inheritDoc} + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index b331be72b4cf1..fb532e2dd17f2 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -19,6 +19,7 @@ package android.widget; import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.NonNull; +import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; @@ -75,6 +76,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; import android.widget.RemoteViews.OnClickHandler; @@ -5982,6 +5984,11 @@ public abstract class AbsListView extends AdapterView implements Te public void closeConnection() { getTarget().closeConnection(); } + + @Override + public boolean insertContent(InputContentInfo inputContentInfo, Bundle opts) { + return getTarget().insertContent(inputContentInfo, opts); + } } /** diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index 59b4b35fa24ef..fc189a3e909e6 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -33,6 +33,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionInspector; import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; +import android.view.inputmethod.InputContentInfo; public abstract class IInputConnectionWrapper extends IInputContext.Stub { static final String TAG = "IInputConnectionWrapper"; @@ -61,6 +62,7 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { private static final int DO_CLEAR_META_KEY_STATES = 130; private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140; private static final int DO_CLOSE_CONNECTION = 150; + private static final int DO_INSERT_CONTENT = 160; @GuardedBy("mLock") @Nullable @@ -241,6 +243,11 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { dispatchMessage(obtainMessage(DO_CLOSE_CONNECTION)); } + public void insertContent(InputContentInfo inputContentInfo, Bundle opts, + int seq, IInputContextCallback callback) { + dispatchMessage(obtainMessageOOSC(DO_INSERT_CONTENT, inputContentInfo, opts, seq, callback)); + } + void dispatchMessage(Message msg) { // If we are calling this from the main thread, then we can call // right through. Otherwise, we need to send the message to the @@ -552,6 +559,29 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { } return; } + case DO_INSERT_CONTENT: { + SomeArgs args = (SomeArgs) msg.obj; + try { + InputConnection ic = getInputConnection(); + if (ic == null || !isActive()) { + Log.w(TAG, "insertContent on inactive InputConnection"); + args.callback.setInsertContentResult(false, args.seq); + return; + } + final InputContentInfo inputContentInfo = (InputContentInfo) args.arg1; + if (inputContentInfo == null || !inputContentInfo.validate()) { + Log.w(TAG, "insertContent with invalid inputContentInfo=" + + inputContentInfo); + args.callback.setInsertContentResult(false, args.seq); + return; + } + args.callback.setInsertContentResult( + ic.insertContent(inputContentInfo, (Bundle) args.arg2), args.seq); + } catch (RemoteException e) { + Log.w(TAG, "Got RemoteException calling insertContent", e); + } + return; + } } Log.w(TAG, "Unhandled message code: " + msg.what); } @@ -582,9 +612,11 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { return mH.obtainMessage(what, arg1, arg2, args); } - Message obtainMessageOSC(int what, Object arg1, int seq, IInputContextCallback callback) { + Message obtainMessageOOSC(int what, Object arg1, Object arg2, int seq, + IInputContextCallback callback) { SomeArgs args = new SomeArgs(); args.arg1 = arg1; + args.arg2 = arg2; args.callback = callback; args.seq = seq; return mH.obtainMessage(what, 0, 0, args); diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl index f7ec420ba6769..bd473557a1b61 100644 --- a/core/java/com/android/internal/view/IInputContext.aidl +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -21,6 +21,7 @@ import android.view.KeyEvent; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputContentInfo; import com.android.internal.view.IInputContextCallback; @@ -74,6 +75,9 @@ import com.android.internal.view.IInputContextCallback; void getSelectedText(int flags, int seq, IInputContextCallback callback); - void requestUpdateCursorAnchorInfo(in int cursorUpdateMode, int seq, + void requestUpdateCursorAnchorInfo(int cursorUpdateMode, int seq, + IInputContextCallback callback); + + void insertContent(in InputContentInfo inputContentInfo, in Bundle opts, int sec, IInputContextCallback callback); } diff --git a/core/java/com/android/internal/view/IInputContextCallback.aidl b/core/java/com/android/internal/view/IInputContextCallback.aidl index 54ea3064bd7e6..933cdc0d87453 100644 --- a/core/java/com/android/internal/view/IInputContextCallback.aidl +++ b/core/java/com/android/internal/view/IInputContextCallback.aidl @@ -28,4 +28,5 @@ oneway interface IInputContextCallback { void setExtractedText(in ExtractedText extractedText, int seq); void setSelectedText(CharSequence selectedText, int seq); void setRequestUpdateCursorAnchorInfoResult(boolean result, int seq); + void setInsertContentResult(boolean result, int seq); } diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index f9884d85d52cb..6d4d45c7041f1 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -16,6 +16,7 @@ package com.android.internal.view; +import android.content.ClipData; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; @@ -29,6 +30,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionInspector; import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; +import android.view.inputmethod.InputContentInfo; public class InputConnectionWrapper implements InputConnection { private static final int MAX_WAIT_TIME_MILLIS = 2000; @@ -46,7 +48,8 @@ public class InputConnectionWrapper implements InputConnection { public ExtractedText mExtractedText; public int mCursorCapsMode; public boolean mRequestUpdateCursorAnchorInfoResult; - + public boolean mInsertContentResult; + // A 'pool' of one InputContextCallback. Each ICW request will attempt to gain // exclusive access to this object. private static InputContextCallback sInstance = new InputContextCallback(); @@ -172,6 +175,19 @@ public class InputConnectionWrapper implements InputConnection { } } + public void setInsertContentResult(boolean result, int seq) { + synchronized (this) { + if (seq == mSeq) { + mInsertContentResult = result; + mHaveValue = true; + notifyAll(); + } else { + Log.i(TAG, "Got out-of-sequence callback " + seq + " (expected " + mSeq + + ") in setInsertContentResult, ignoring."); + } + } + } + /** * Waits for a result for up to {@link #MAX_WAIT_TIME_MILLIS} milliseconds. * @@ -491,6 +507,28 @@ public class InputConnectionWrapper implements InputConnection { // Nothing should happen when called from input method. } + public boolean insertContent(InputContentInfo inputContentInfo, Bundle opts) { + boolean result = false; + if (isMethodMissing(MissingMethodFlags.INSERT_CONTENT)) { + // This method is not implemented. + return false; + } + try { + InputContextCallback callback = InputContextCallback.getInstance(); + mIInputContext.insertContent(inputContentInfo, opts, callback.mSeq, callback); + synchronized (callback) { + callback.waitForResultLocked(); + if (callback.mHaveValue) { + result = callback.mInsertContentResult; + } + } + callback.dispose(); + } catch (RemoteException e) { + return false; + } + return result; + } + private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) { return (mMissingMethods & methodFlag) == methodFlag; }