diff --git a/core/api/current.txt b/core/api/current.txt index faeb19b452d0f..8f63dea516117 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -52258,6 +52258,7 @@ package android.view.inputmethod { method public boolean commitContent(@NonNull android.view.inputmethod.InputContentInfo, int, @Nullable android.os.Bundle); method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo); method public boolean commitText(CharSequence, int); + method public default boolean commitText(@NonNull CharSequence, int, @Nullable android.view.inputmethod.TextAttribute); method public boolean deleteSurroundingText(int, int); method public boolean deleteSurroundingTextInCodePoints(int, int); method public boolean endBatchEdit(); @@ -52277,7 +52278,9 @@ package android.view.inputmethod { method public boolean requestCursorUpdates(int); method public boolean sendKeyEvent(android.view.KeyEvent); method public boolean setComposingRegion(int, int); + method public default boolean setComposingRegion(int, int, @Nullable android.view.inputmethod.TextAttribute); method public boolean setComposingText(CharSequence, int); + method public default boolean setComposingText(@NonNull CharSequence, int, @Nullable android.view.inputmethod.TextAttribute); method public default boolean setImeConsumesInput(boolean); method public boolean setSelection(int, int); method @Nullable public default android.view.inputmethod.TextSnapshot takeSnapshot(); @@ -52493,6 +52496,21 @@ package android.view.inputmethod { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public final class TextAttribute implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.os.PersistableBundle getExtras(); + method @NonNull public java.util.List getTextConversionSuggestions(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class TextAttribute.TextAttributeBuilder { + ctor public TextAttribute.TextAttributeBuilder(); + method @NonNull public android.view.inputmethod.TextAttribute build(); + method @NonNull public android.view.inputmethod.TextAttribute.TextAttributeBuilder setExtras(@NonNull android.os.PersistableBundle); + method @NonNull public android.view.inputmethod.TextAttribute.TextAttributeBuilder setTextConversionSuggestions(@NonNull java.util.List); + } + public final class TextSnapshot { ctor public TextSnapshot(@NonNull android.view.inputmethod.SurroundingText, @IntRange(from=0xffffffff) int, @IntRange(from=0xffffffff) int, int); method @IntRange(from=0xffffffff) public int getCompositionEnd(); diff --git a/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java index ae97fe7df7293..ed617afab96e9 100644 --- a/core/java/android/inputmethodservice/RemoteInputConnection.java +++ b/core/java/android/inputmethodservice/RemoteInputConnection.java @@ -31,6 +31,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextAttribute; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.CompletableFutureUtil; @@ -271,6 +272,17 @@ final class RemoteInputConnection implements InputConnection { return handled; } + @AnyThread + public boolean commitText(@NonNull CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + final boolean handled = + mInvoker.commitText(text, newCursorPosition, textAttribute); + if (handled) { + notifyUserActionIfNecessary(); + } + return handled; + } + @AnyThread private void notifyUserActionIfNecessary() { final InputMethodServiceInternal imsInternal = mImsInternal.getAndWarnIfNull(); @@ -310,6 +322,11 @@ final class RemoteInputConnection implements InputConnection { return mInvoker.setComposingRegion(start, end); } + @AnyThread + public boolean setComposingRegion(int start, int end, @Nullable TextAttribute textAttribute) { + return mInvoker.setComposingRegion(start, end, textAttribute); + } + @AnyThread public boolean setComposingText(CharSequence text, int newCursorPosition) { final boolean handled = mInvoker.setComposingText(text, newCursorPosition); @@ -319,6 +336,16 @@ final class RemoteInputConnection implements InputConnection { return handled; } + @AnyThread + public boolean setComposingText(CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + final boolean handled = mInvoker.setComposingText(text, newCursorPosition, textAttribute); + if (handled) { + notifyUserActionIfNecessary(); + } + return handled; + } + @AnyThread public boolean finishComposingText() { return mInvoker.finishComposingText(); diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index c3d7836a4786b..3b15db2ded70f 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -544,6 +544,33 @@ public interface InputConnection { */ boolean setComposingText(CharSequence text, int newCursorPosition); + /** + * The variant of {@link #setComposingText(CharSequence, int)}. This method is + * used to allow the IME to provide extra information while setting up composing text. + * + * @param text The composing text with styles if necessary. If no style + * object attached to the text, the default style for composing text + * is used. See {@link android.text.Spanned} for how to attach style + * object to the text. {@link android.text.SpannableString} and + * {@link android.text.SpannableStringBuilder} are two + * implementations of the interface {@link android.text.Spanned}. + * @param newCursorPosition The new cursor position around the text. If + * > 0, this is relative to the end of the text - 1; if <= 0, this + * is relative to the start of the text. So a value of 1 will + * always advance you to the position after the full text being + * inserted. Note that this means you can't position the cursor + * within the text, because the editor can make modifications to + * the text you are providing so it is not possible to correctly + * specify locations there. + * @param textAttribute The extra information about the text. + * @return true on success, false if the input connection is no longer + * + */ + default boolean setComposingText(@NonNull CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + return setComposingText(text, newCursorPosition); + } + /** * Mark a certain region of text as composing text. If there was a * composing region, the characters are left as they were and the @@ -578,6 +605,22 @@ public interface InputConnection { */ boolean setComposingRegion(int start, int end); + /** + * The variant of {@link InputConnection#setComposingRegion(int, int)}. This method is + * used to allow the IME to provide extra information while setting up text. + * + * @param start the position in the text at which the composing region begins + * @param end the position in the text at which the composing region ends + * @param textAttribute The extra information about the text. + * @return {@code true} on success, {@code false} if the input connection is no longer valid. + * Since Android {@link android.os.Build.VERSION_CODES#N} until + * {@link android.os.Build.VERSION_CODES#TIRAMISU}, this API returned {@code false} when + * the target application does not implement this method. + */ + default boolean setComposingRegion(int start, int end, @Nullable TextAttribute textAttribute) { + return setComposingRegion(start, end); + } + /** * Have the text editor finish whatever composing text is * currently active. This simply leaves the text as-is, removing @@ -633,6 +676,28 @@ public interface InputConnection { */ boolean commitText(CharSequence text, int newCursorPosition); + /** + * The variant of {@link InputConnection#commitText(CharSequence, int)}. This method is + * used to allow the IME to provide extra information while setting up text. + * + * @param text The text to commit. This may include styles. + * @param newCursorPosition The new cursor position around the text, + * in Java characters. If > 0, this is relative to the end + * of the text - 1; if <= 0, this is relative to the start + * of the text. So a value of 1 will always advance the cursor + * to the position after the full text being inserted. Note that + * this means you can't position the cursor within the text, + * because the editor can make modifications to the text + * you are providing so it is not possible to correctly specify + * locations there. + * @param textAttribute The extra information about the text. + * @return true on success, false if the input connection is no longer + */ + default boolean commitText(@NonNull CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + return commitText(text, newCursorPosition); + } + /** * Commit a completion the user has selected from the possible ones * previously reported to {@link InputMethodSession#displayCompletions diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index a99e9b8aab077..7a88a75f93ade 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -17,6 +17,7 @@ package android.view.inputmethod; import android.annotation.IntRange; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Bundle; import android.os.Handler; @@ -153,6 +154,16 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.setComposingText(text, newCursorPosition); } + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + @Override + public boolean setComposingText(@NonNull CharSequence text, + int newCursorPosition, @Nullable TextAttribute textAttribute) { + return mTarget.setComposingText(text, newCursorPosition, textAttribute); + } + /** * {@inheritDoc} * @throws NullPointerException if the target is {@code null}. @@ -162,6 +173,15 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.setComposingRegion(start, end); } + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + @Override + public boolean setComposingRegion(int start, int end, @Nullable TextAttribute textAttribute) { + return mTarget.setComposingRegion(start, end, textAttribute); + } + /** * {@inheritDoc} * @throws NullPointerException if the target is {@code null}. @@ -180,6 +200,16 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.commitText(text, newCursorPosition); } + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + @Override + public boolean commitText(@NonNull CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + return mTarget.commitText(text, newCursorPosition, textAttribute); + } + /** * {@inheritDoc} * @throws NullPointerException if the target is {@code null}. diff --git a/core/java/android/view/inputmethod/TextAttribute.aidl b/core/java/android/view/inputmethod/TextAttribute.aidl new file mode 100644 index 0000000000000..5f296d9cac743 --- /dev/null +++ b/core/java/android/view/inputmethod/TextAttribute.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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 TextAttribute; \ No newline at end of file diff --git a/core/java/android/view/inputmethod/TextAttribute.java b/core/java/android/view/inputmethod/TextAttribute.java new file mode 100644 index 0000000000000..bc76e780a9e12 --- /dev/null +++ b/core/java/android/view/inputmethod/TextAttribute.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2021 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.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The data class that IME can take extra information to applications when setting the text. + * + * See {@link InputConnection#commitText(CharSequence, int, TextAttribute)} and + * {@link InputConnection#setComposingRegion(int, int, TextAttribute)} and + * {@link InputConnection#setComposingText(CharSequence, int, TextAttribute)} + */ +public final class TextAttribute implements Parcelable { + private final @NonNull List mTextConversionSuggestions; + private final @NonNull PersistableBundle mExtras; + + private TextAttribute(TextAttributeBuilder builder) { + mTextConversionSuggestions = builder.mTextConversionSuggestions; + mExtras = builder.mExtras; + } + + private TextAttribute(Parcel source) { + mTextConversionSuggestions = source.createStringArrayList(); + mExtras = source.readPersistableBundle(); + } + + /** + * Get the list of text conversion suggestions. More text conversion details in + * {@link TextAttributeBuilder#setTextConversionSuggestions(List)}. + * + * @return List of text conversion suggestions. If the list is empty, it means that IME not set + * this field or IME didn't have suggestions for applications. + */ + public @NonNull List getTextConversionSuggestions() { + return mTextConversionSuggestions; + } + + /** + * Get the extras data. More extras data details in + * {@link TextAttributeBuilder#setExtras(PersistableBundle)}. + * + * @return Extras data. If the Bundle is empty, it means that IME not set this field or IME + * didn't have extras data. + */ + public @NonNull PersistableBundle getExtras() { + return mExtras; + } + + /** + * Builder for creating a {@link TextAttribute}. + */ + public static final class TextAttributeBuilder { + private List mTextConversionSuggestions = new ArrayList<>(); + private PersistableBundle mExtras = new PersistableBundle(); + + /** + * Sets text conversion suggestions. + * + *

Text conversion suggestion is for some transliteration languages which has + * pronunciation characters and target characters. When the user is typing the pronunciation + * characters, the input method can insert possible target characters into this list so that + * the editor authors can provide suggestion before the user enters the complete + * pronunciation characters.

+ * + * @param textConversionSuggestions The list of text conversion suggestions. + * @return This builder + */ + public @NonNull TextAttributeBuilder setTextConversionSuggestions( + @NonNull List textConversionSuggestions) { + mTextConversionSuggestions = Collections.unmodifiableList(textConversionSuggestions); + return this; + } + + /** + * Sets extras data. + * + *

Any extra data to supply to the applications. This field is for extended communication + * with IME if there is data not defined in framework.

+ * + * @return This builder. + */ + public @NonNull TextAttributeBuilder setExtras(@NonNull PersistableBundle extras) { + mExtras = extras; + return this; + } + + /** + * @return a new {@link TextAttribute}. + */ + public @NonNull TextAttribute build() { + return new TextAttribute(this); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeStringList(mTextConversionSuggestions); + dest.writePersistableBundle(mExtras); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public TextAttribute createFromParcel(Parcel source) { + return new TextAttribute(source); + } + + @Override + public TextAttribute[] newArray(int size) { + return new TextAttribute[size]; + } + }; +} diff --git a/core/java/com/android/internal/inputmethod/IInputContextInvoker.java b/core/java/com/android/internal/inputmethod/IInputContextInvoker.java index efdf483563ec1..4dbd941b3ae07 100644 --- a/core/java/com/android/internal/inputmethod/IInputContextInvoker.java +++ b/core/java/com/android/internal/inputmethod/IInputContextInvoker.java @@ -18,6 +18,7 @@ package com.android.internal.inputmethod; import android.annotation.AnyThread; import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Bundle; import android.os.RemoteException; import android.view.KeyEvent; @@ -27,6 +28,7 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextAttribute; import com.android.internal.infra.AndroidFuture; import com.android.internal.view.IInputContext; @@ -210,6 +212,28 @@ public final class IInputContextInvoker { } } + /** + * Invokes {@link IInputContext#commitTextWithTextAttribute(InputConnectionCommandHeader, int, + * CharSequence)}. + * + * @param text {@code text} parameter to be passed. + * @param newCursorPosition {@code newCursorPosition} parameter to be passed. + * @param textAttribute The extra information about the text. + * @return {@code true} if the invocation is completed without {@link RemoteException}. + * {@code false} otherwise. + */ + @AnyThread + public boolean commitText(CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + try { + mIInputContext.commitTextWithTextAttribute( + createHeader(), text, newCursorPosition, textAttribute); + return true; + } catch (RemoteException e) { + return false; + } + } + /** * Invokes {@link IInputContext#commitCompletion(InputConnectionCommandHeader, CompletionInfo)}. * @@ -314,6 +338,27 @@ public final class IInputContextInvoker { } } + /** + * Invokes {@link IInputContext#setComposingRegionWithTextAttribute( + * InputConnectionCommandHeader, int, int, TextAttribute)}. + * + * @param start {@code id} parameter to be passed. + * @param end {@code id} parameter to be passed. + * @param textAttribute The extra information about the text. + * @return {@code true} if the invocation is completed without {@link RemoteException}. + * {@code false} otherwise. + */ + @AnyThread + public boolean setComposingRegion(int start, int end, @Nullable TextAttribute textAttribute) { + try { + mIInputContext.setComposingRegionWithTextAttribute( + createHeader(), start, end, textAttribute); + return true; + } catch (RemoteException e) { + return false; + } + } + /** * Invokes * {@link IInputContext#setComposingText(InputConnectionCommandHeader, CharSequence, int)}. @@ -333,6 +378,28 @@ public final class IInputContextInvoker { } } + /** + * Invokes {@link IInputContext#setComposingTextWithTextAttribute(InputConnectionCommandHeader, + * CharSequence, int, TextAttribute)}. + * + * @param text {@code text} parameter to be passed. + * @param newCursorPosition {@code newCursorPosition} parameter to be passed. + * @param textAttribute The extra information about the text. + * @return {@code true} if the invocation is completed without {@link RemoteException}. + * {@code false} otherwise. + */ + @AnyThread + public boolean setComposingText(CharSequence text, int newCursorPosition, + @Nullable TextAttribute textAttribute) { + try { + mIInputContext.setComposingTextWithTextAttribute( + createHeader(), text, newCursorPosition, textAttribute); + return true; + } catch (RemoteException e) { + return false; + } + } + /** * Invokes {@link IInputContext#finishComposingText(InputConnectionCommandHeader)}. * diff --git a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java index 21358abe243f3..550322627794a 100644 --- a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java +++ b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java @@ -42,6 +42,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.TextAttribute; import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AndroidFuture; @@ -383,6 +384,23 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub { }); } + @Dispatching(cancellable = true) + @Override + public void commitTextWithTextAttribute(InputConnectionCommandHeader header, CharSequence text, + int newCursorPosition, @Nullable TextAttribute textAttribute) { + dispatchWithTracing("commitTextWithTextAttribute", () -> { + if (header.mSessionId != mCurrentSessionId.get()) { + return; // cancelled + } + InputConnection ic = getInputConnection(); + if (ic == null || !isActive()) { + Log.w(TAG, "commitText on inactive InputConnection"); + return; + } + ic.commitText(text, newCursorPosition, textAttribute); + }); + } + @Dispatching(cancellable = true) @Override public void commitCompletion(InputConnectionCommandHeader header, CompletionInfo text) { @@ -487,6 +505,23 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub { }); } + @Dispatching(cancellable = true) + @Override + public void setComposingRegionWithTextAttribute(InputConnectionCommandHeader header, int start, + int end, @Nullable TextAttribute textAttribute) { + dispatchWithTracing("setComposingRegionWithTextAttribute", () -> { + if (header.mSessionId != mCurrentSessionId.get()) { + return; // cancelled + } + InputConnection ic = getInputConnection(); + if (ic == null || !isActive()) { + Log.w(TAG, "setComposingRegion on inactive InputConnection"); + return; + } + ic.setComposingRegion(start, end, textAttribute); + }); + } + @Dispatching(cancellable = true) @Override public void setComposingText(InputConnectionCommandHeader header, CharSequence text, @@ -504,6 +539,23 @@ public final class RemoteInputConnectionImpl extends IInputContext.Stub { }); } + @Dispatching(cancellable = true) + @Override + public void setComposingTextWithTextAttribute(InputConnectionCommandHeader header, + CharSequence text, int newCursorPosition, @Nullable TextAttribute textAttribute) { + dispatchWithTracing("setComposingTextWithTextAttribute", () -> { + if (header.mSessionId != mCurrentSessionId.get()) { + return; // cancelled + } + InputConnection ic = getInputConnection(); + if (ic == null || !isActive()) { + Log.w(TAG, "setComposingText on inactive InputConnection"); + return; + } + ic.setComposingText(text, newCursorPosition, textAttribute); + }); + } + /** * Dispatches {@link InputConnection#finishComposingText()}. * diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl index df55bebb35efc..7da0f116c358d 100644 --- a/core/java/com/android/internal/view/IInputContext.aidl +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -22,6 +22,7 @@ import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.TextAttribute; import com.android.internal.infra.AndroidFuture; import com.android.internal.inputmethod.InputConnectionCommandHeader; @@ -52,10 +53,16 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader; void setComposingText(in InputConnectionCommandHeader header, CharSequence text, int newCursorPosition); + void setComposingTextWithTextAttribute(in InputConnectionCommandHeader header, + CharSequence text, int newCursorPosition, in TextAttribute textAttribute); + void finishComposingText(in InputConnectionCommandHeader header); void commitText(in InputConnectionCommandHeader header, CharSequence text, - int newCursorPosition); + int newCursorPosition); + + void commitTextWithTextAttribute(in InputConnectionCommandHeader header, CharSequence text, + int newCursorPosition, in TextAttribute textAttribute); void commitCompletion(in InputConnectionCommandHeader header, in CompletionInfo completion); @@ -82,6 +89,9 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader; void setComposingRegion(in InputConnectionCommandHeader header, int start, int end); + void setComposingRegionWithTextAttribute(in InputConnectionCommandHeader header, int start, + int end, in TextAttribute textAttribute); + void getSelectedText(in InputConnectionCommandHeader header, int flags, in AndroidFuture future /* T=CharSequence */);