Tell IMS about missing InputConnection methods.
Summary:
This CL introduces a unified mechanism to deal with the situation
where the application directly implements InputConnection but some of
methods are not implemented. Note that there should be zero overhead
when the application extends BaseInputConnection or
InputConnectionWrapper.
Background:
When ever we add a new method to InputConnection, there has been a
risk that existing applications that directly implement
InputConnection can get java.lang.AbstractMethodError exception at
runtime, because older SDKs do not require the application developer
to implement the methods that are newly added in later SDKs. Because
of this we strongly discouraged developers to directly implement
InputConnection interface, and encouraged them to subclass
BaseInputConnection or InputConnectionWrapper instead. That said, as
requested in Bug 26945674, there is a certain demand to be able to
implement InputConnection without depending on BaseInputConnection.
The goal of this CL is to provide a reliable and sustainable solution
to above missing method scenario in InputConnection.
One of the reasons why dealing with missing InputConnection methods is
so difficult is that what InputMethodService receives to communicate
with the target application is actually a proxy class
com.android.internal.view.InputConnectionWrapper
that runs in the IME process and immediately returns true for most of
methods in InputConnection such as #commitText() and
#finishComposingText(). Because of this asynchronous nature, it is
too late to change the actual return value that the IME receives when
the application receives those one-way asynchronous IPC calls.
Solution:
To handle those cases, this CL checks the availability of
InputConnection methods that did not exist in the initial release
before the target application calls startInput(), and let the
application to send its availability bits to IMMS so that
InputConnectionWrapper running in the IME process can be initialized
with such availability bits. Note that we do know that
BaseInputConnection and its subclasses support all the InputConnection
methods, hence for most of applications we can just assume that all
the methods are available without reflection.
With such availability bits, InputConnectionWrapper is now able to
gracefully return failure code to the IME because the availability of
those methods is immutable, except for a tricky case where the
application relies on a proxy object that dynamically changes the
dispatch target.
Here is the list of APIs that we start checking the availability in
this CL.
[API Level 9+]
- InputConnection#getSelectedText(int)
- InputConnection#setComposingRegion(int, int)
[API Level 11+]
- InputConnection#commitCorrection(CorrectionInfo)
[API Level 21+]
- InputConnection#requestCursorUpdates(int)}
[API Level 24+]
- InputConnection#deleteSurroundingTextInCodePoints(int, int)
- InputConnection#getHandler()
Ideas alternatively considered: Default methods in InputConnection
We once considered having default methods in InputConnection but
abandoned this idea because it does not directly solve the problem
about how to tell the that the API does not take effect.
Also having default methods would make it difficult for application
developers to be aware of newly added methods in InputConnection.
Bug: 27407234
Bug: 27642734
Bug: 27650039
Change-Id: I3c58fadd924fad72cb984f0c23d3099fd0295c64
This commit is contained in:
@@ -36,6 +36,7 @@ import android.view.InputChannel;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputBinding;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputConnectionInspector;
|
||||
import android.view.inputmethod.InputMethod;
|
||||
import android.view.inputmethod.InputMethodSession;
|
||||
import android.view.inputmethod.InputMethodSubtype;
|
||||
@@ -164,9 +165,10 @@ class IInputMethodWrapper extends IInputMethod.Stub
|
||||
return;
|
||||
case DO_START_INPUT: {
|
||||
SomeArgs args = (SomeArgs)msg.obj;
|
||||
int missingMethods = msg.arg1;
|
||||
IInputContext inputContext = (IInputContext)args.arg1;
|
||||
InputConnection ic = inputContext != null
|
||||
? new InputConnectionWrapper(inputContext) : null;
|
||||
? new InputConnectionWrapper(inputContext, missingMethods) : null;
|
||||
EditorInfo info = (EditorInfo)args.arg2;
|
||||
info.makeCompatible(mTargetSdkVersion);
|
||||
inputMethod.startInput(ic, info);
|
||||
@@ -175,9 +177,10 @@ class IInputMethodWrapper extends IInputMethod.Stub
|
||||
}
|
||||
case DO_RESTART_INPUT: {
|
||||
SomeArgs args = (SomeArgs)msg.obj;
|
||||
int missingMethods = msg.arg1;
|
||||
IInputContext inputContext = (IInputContext)args.arg1;
|
||||
InputConnection ic = inputContext != null
|
||||
? new InputConnectionWrapper(inputContext) : null;
|
||||
? new InputConnectionWrapper(inputContext, missingMethods) : null;
|
||||
EditorInfo info = (EditorInfo)args.arg2;
|
||||
info.makeCompatible(mTargetSdkVersion);
|
||||
inputMethod.restartInput(ic, info);
|
||||
@@ -246,8 +249,10 @@ class IInputMethodWrapper extends IInputMethod.Stub
|
||||
|
||||
@Override
|
||||
public void bindInput(InputBinding binding) {
|
||||
// This IInputContext is guaranteed to implement all the methods.
|
||||
final int missingMethodFlags = 0;
|
||||
InputConnection ic = new InputConnectionWrapper(
|
||||
IInputContext.Stub.asInterface(binding.getConnectionToken()));
|
||||
IInputContext.Stub.asInterface(binding.getConnectionToken()), missingMethodFlags);
|
||||
InputBinding nu = new InputBinding(ic, binding);
|
||||
mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_INPUT_CONTEXT, nu));
|
||||
}
|
||||
@@ -258,15 +263,19 @@ class IInputMethodWrapper extends IInputMethod.Stub
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startInput(IInputContext inputContext, EditorInfo attribute) {
|
||||
mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_START_INPUT,
|
||||
inputContext, attribute));
|
||||
public void startInput(IInputContext inputContext,
|
||||
@InputConnectionInspector.MissingMethodFlags final int missingMethods,
|
||||
EditorInfo attribute) {
|
||||
mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_START_INPUT,
|
||||
missingMethods, inputContext, attribute));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restartInput(IInputContext inputContext, EditorInfo attribute) {
|
||||
mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_RESTART_INPUT,
|
||||
inputContext, attribute));
|
||||
public void restartInput(IInputContext inputContext,
|
||||
@InputConnectionInspector.MissingMethodFlags final int missingMethods,
|
||||
EditorInfo attribute) {
|
||||
mCaller.executeOrSendMessage(mCaller.obtainMessageIOO(DO_RESTART_INPUT,
|
||||
missingMethods, inputContext, attribute));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,10 +28,24 @@ import android.view.KeyEvent;
|
||||
* cursor, committing text to the text box, and sending raw key events
|
||||
* to the application.
|
||||
*
|
||||
* <p>Applications should never directly implement this interface, but
|
||||
* instead subclass from {@link BaseInputConnection}. This will ensure
|
||||
* that the application does not break when new methods are added to
|
||||
* the interface.</p>
|
||||
* <p>Starting from API Level {@link android.os.Build.VERSION_CODES#N},
|
||||
* the system can deal with the situation where the application directly
|
||||
* implements this class but one or more of the following methods are
|
||||
* not implemented.</p>
|
||||
* <ul>
|
||||
* <li>{@link #getSelectedText(int)}, which was introduced in
|
||||
* {@link android.os.Build.VERSION_CODES#GINGERBREAD}.</li>
|
||||
* <li>{@link #setComposingRegion(int, int)}, which was introduced
|
||||
* in {@link android.os.Build.VERSION_CODES#GINGERBREAD}.</li>
|
||||
* <li>{@link #commitCorrection(CorrectionInfo)}, which was introduced
|
||||
* in {@link android.os.Build.VERSION_CODES#HONEYCOMB}.</li>
|
||||
* <li>{@link #requestCursorUpdates(int)}, which was introduced in
|
||||
* {@link android.os.Build.VERSION_CODES#LOLLIPOP}.</li>
|
||||
* <li>{@link #deleteSurroundingTextInCodePoints(int, int)}}, which
|
||||
* was introduced in {@link android.os.Build.VERSION_CODES#N}.</li>
|
||||
* <li>{@link #getHandler()}}, which was introduced in
|
||||
* {@link android.os.Build.VERSION_CODES#N}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h3>Implementing an IME or an editor</h3>
|
||||
* <p>Text input is the result of the synergy of two essential components:
|
||||
@@ -224,7 +238,9 @@ public interface InputConnection {
|
||||
* @param flags Supplies additional options controlling how the text is
|
||||
* returned. May be either 0 or {@link #GET_TEXT_WITH_STYLES}.
|
||||
* @return the text that is currently selected, if any, or null if
|
||||
* no text is selected.
|
||||
* no text is selected. In {@link android.os.Build.VERSION_CODES#N} and
|
||||
* later, returns false when the target application does not implement
|
||||
* this method.
|
||||
*/
|
||||
public CharSequence getSelectedText(int flags);
|
||||
|
||||
@@ -371,7 +387,8 @@ public interface InputConnection {
|
||||
* If this is greater than the number of existing characters between the cursor and
|
||||
* the end of the text, then this method does not fail but deletes all the characters in
|
||||
* that range.
|
||||
* @return true on success, false if the input connection is no longer valid.
|
||||
* @return true on success, false if the input connection is no longer valid. Returns
|
||||
* {@code false} when the target application does not implement this method.
|
||||
*/
|
||||
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
|
||||
|
||||
@@ -461,7 +478,8 @@ public interface InputConnection {
|
||||
* @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
|
||||
* @return true on success, false if the input connection is no longer
|
||||
* valid.
|
||||
* valid. In {@link android.os.Build.VERSION_CODES#N} and later, false is returned when the
|
||||
* target application does not implement this method.
|
||||
*/
|
||||
public boolean setComposingRegion(int start, int end);
|
||||
|
||||
@@ -573,6 +591,8 @@ public interface InputConnection {
|
||||
*
|
||||
* @param correctionInfo Detailed information about the correction.
|
||||
* @return true on success, false if the input connection is no longer valid.
|
||||
* In {@link android.os.Build.VERSION_CODES#N} and later, returns false
|
||||
* when the target application does not implement this method.
|
||||
*/
|
||||
public boolean commitCorrection(CorrectionInfo correctionInfo);
|
||||
|
||||
@@ -785,6 +805,8 @@ public interface InputConnection {
|
||||
* @return {@code true} if the request is scheduled. {@code false} to indicate that when the
|
||||
* application will not call
|
||||
* {@link InputMethodManager#updateCursorAnchorInfo(android.view.View, CursorAnchorInfo)}.
|
||||
* In {@link android.os.Build.VERSION_CODES#N} and later, returns {@code false} also when the
|
||||
* target application does not implement this method.
|
||||
*/
|
||||
public boolean requestCursorUpdates(int cursorUpdateMode);
|
||||
|
||||
|
||||
224
core/java/android/view/inputmethod/InputConnectionInspector.java
Normal file
224
core/java/android/view/inputmethod/InputConnectionInspector.java
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* 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.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public final class InputConnectionInspector {
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({MissingMethodFlags.GET_SELECTED_TEXT,
|
||||
MissingMethodFlags.SET_COMPOSING_REGION,
|
||||
MissingMethodFlags.COMMIT_CORRECTION,
|
||||
MissingMethodFlags.REQUEST_CURSOR_UPDATES,
|
||||
MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS,
|
||||
MissingMethodFlags.GET_HANDLER,
|
||||
})
|
||||
public @interface MissingMethodFlags {
|
||||
/**
|
||||
* {@link InputConnection#getSelectedText(int)} is available in
|
||||
* {@link android.os.Build.VERSION_CODES#GINGERBREAD} and later.
|
||||
*/
|
||||
int GET_SELECTED_TEXT = 1 << 0;
|
||||
/**
|
||||
* {@link InputConnection#setComposingRegion(int, int)} is available in
|
||||
* {@link android.os.Build.VERSION_CODES#GINGERBREAD} and later.
|
||||
*/
|
||||
int SET_COMPOSING_REGION = 1 << 1;
|
||||
/**
|
||||
* {@link InputConnection#commitCorrection(CorrectionInfo)} is available in
|
||||
* {@link android.os.Build.VERSION_CODES#HONEYCOMB} and later.
|
||||
*/
|
||||
int COMMIT_CORRECTION = 1 << 2;
|
||||
/**
|
||||
* {@link InputConnection#requestCursorUpdates(int)} is available in
|
||||
* {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later.
|
||||
*/
|
||||
int REQUEST_CURSOR_UPDATES = 1 << 3;
|
||||
/**
|
||||
* {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}} is available in
|
||||
* {@link android.os.Build.VERSION_CODES#N} and later.
|
||||
*/
|
||||
int DELETE_SURROUNDING_TEXT_IN_CODE_POINTS = 1 << 4;
|
||||
/**
|
||||
* {@link InputConnection#deleteSurroundingTextInCodePoints(int, int)}} is available in
|
||||
* {@link android.os.Build.VERSION_CODES#N} and later.
|
||||
*/
|
||||
int GET_HANDLER = 1 << 5;
|
||||
}
|
||||
|
||||
private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap(
|
||||
new WeakHashMap<>());
|
||||
|
||||
@MissingMethodFlags
|
||||
public static int getMissingMethodFlags(@Nullable final InputConnection ic) {
|
||||
if (ic == null) {
|
||||
return 0;
|
||||
}
|
||||
// Optimization for a known class.
|
||||
if (ic instanceof BaseInputConnection) {
|
||||
return 0;
|
||||
}
|
||||
// Optimization for a known class.
|
||||
if (ic instanceof InputConnectionWrapper) {
|
||||
return ((InputConnectionWrapper) ic).getMissingMethodFlags();
|
||||
}
|
||||
return getMissingMethodFlagsInternal(ic.getClass());
|
||||
}
|
||||
|
||||
@MissingMethodFlags
|
||||
public static int getMissingMethodFlagsInternal(@NonNull final Class clazz) {
|
||||
final Integer cachedFlags = sMissingMethodsMap.get(clazz);
|
||||
if (cachedFlags != null) {
|
||||
return cachedFlags;
|
||||
}
|
||||
int flags = 0;
|
||||
if (!hasGetSelectedText(clazz)) {
|
||||
flags |= MissingMethodFlags.GET_SELECTED_TEXT;
|
||||
}
|
||||
if (!hasSetComposingRegion(clazz)) {
|
||||
flags |= MissingMethodFlags.SET_COMPOSING_REGION;
|
||||
}
|
||||
if (!hasCommitCorrection(clazz)) {
|
||||
flags |= MissingMethodFlags.COMMIT_CORRECTION;
|
||||
}
|
||||
if (!hasRequestCursorUpdate(clazz)) {
|
||||
flags |= MissingMethodFlags.REQUEST_CURSOR_UPDATES;
|
||||
}
|
||||
if (!hasDeleteSurroundingTextInCodePoints(clazz)) {
|
||||
flags |= MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS;
|
||||
}
|
||||
if (!hasGetHandler(clazz)) {
|
||||
flags |= MissingMethodFlags.GET_HANDLER;
|
||||
}
|
||||
sMissingMethodsMap.put(clazz, flags);
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static boolean hasGetSelectedText(@NonNull final Class clazz) {
|
||||
try {
|
||||
final Method method = clazz.getMethod("getSelectedText", int.class);
|
||||
return !Modifier.isAbstract(method.getModifiers());
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasSetComposingRegion(@NonNull final Class clazz) {
|
||||
try {
|
||||
final Method method = clazz.getMethod("setComposingRegion", int.class, int.class);
|
||||
return !Modifier.isAbstract(method.getModifiers());
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasCommitCorrection(@NonNull final Class clazz) {
|
||||
try {
|
||||
final Method method = clazz.getMethod("commitCorrection", CorrectionInfo.class);
|
||||
return !Modifier.isAbstract(method.getModifiers());
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasRequestCursorUpdate(@NonNull final Class clazz) {
|
||||
try {
|
||||
final Method method = clazz.getMethod("requestCursorUpdates", int.class);
|
||||
return !Modifier.isAbstract(method.getModifiers());
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasDeleteSurroundingTextInCodePoints(@NonNull final Class clazz) {
|
||||
try {
|
||||
final Method method = clazz.getMethod("deleteSurroundingTextInCodePoints", int.class,
|
||||
int.class);
|
||||
return !Modifier.isAbstract(method.getModifiers());
|
||||
} catch (NoSuchMethodException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasGetHandler(@NonNull final Class clazz) {
|
||||
try {
|
||||
final Method method = clazz.getMethod("getHandler");
|
||||
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;
|
||||
if ((flags & MissingMethodFlags.GET_SELECTED_TEXT) != 0) {
|
||||
sb.append("getSelectedText(int)");
|
||||
isEmpty = false;
|
||||
}
|
||||
if ((flags & MissingMethodFlags.SET_COMPOSING_REGION) != 0) {
|
||||
if (!isEmpty) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("setComposingRegion(int, int)");
|
||||
isEmpty = false;
|
||||
}
|
||||
if ((flags & MissingMethodFlags.COMMIT_CORRECTION) != 0) {
|
||||
if (!isEmpty) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("commitCorrection(CorrectionInfo)");
|
||||
isEmpty = false;
|
||||
}
|
||||
if ((flags & MissingMethodFlags.REQUEST_CURSOR_UPDATES) != 0) {
|
||||
if (!isEmpty) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("requestCursorUpdate(int)");
|
||||
isEmpty = false;
|
||||
}
|
||||
if ((flags & MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS) != 0) {
|
||||
if (!isEmpty) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("deleteSurroundingTextInCodePoints(int, int)");
|
||||
isEmpty = false;
|
||||
}
|
||||
if ((flags & MissingMethodFlags.GET_HANDLER) != 0) {
|
||||
if (!isEmpty) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("getHandler()");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import android.view.KeyEvent;
|
||||
public class InputConnectionWrapper implements InputConnection {
|
||||
private InputConnection mTarget;
|
||||
final boolean mMutable;
|
||||
@InputConnectionInspector.MissingMethodFlags
|
||||
private int mMissingMethodFlags;
|
||||
|
||||
/**
|
||||
* Initializes a wrapper.
|
||||
@@ -40,6 +42,7 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
public InputConnectionWrapper(InputConnection target, boolean mutable) {
|
||||
mMutable = mutable;
|
||||
mTarget = target;
|
||||
mMissingMethodFlags = InputConnectionInspector.getMissingMethodFlags(target);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +59,15 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
throw new SecurityException("not mutable");
|
||||
}
|
||||
mTarget = target;
|
||||
mMissingMethodFlags = InputConnectionInspector.getMissingMethodFlags(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@InputConnectionInspector.MissingMethodFlags
|
||||
public int getMissingMethodFlags() {
|
||||
return mMissingMethodFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1224,6 +1224,7 @@ public final class InputMethodManager {
|
||||
notifyInputConnectionFinished();
|
||||
mServedInputConnection = ic;
|
||||
ControlledInputConnectionWrapper servedContext;
|
||||
final int missingMethodFlags;
|
||||
if (ic != null) {
|
||||
mCursorSelStart = tba.initialSelStart;
|
||||
mCursorSelEnd = tba.initialSelEnd;
|
||||
@@ -1231,11 +1232,20 @@ public final class InputMethodManager {
|
||||
mCursorCandEnd = -1;
|
||||
mCursorRect.setEmpty();
|
||||
mCursorAnchorInfo = null;
|
||||
final Handler icHandler = ic.getHandler();
|
||||
final Handler icHandler;
|
||||
missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic);
|
||||
if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER)
|
||||
!= 0) {
|
||||
// InputConnection#getHandler() is not implemented.
|
||||
icHandler = null;
|
||||
} else {
|
||||
icHandler = ic.getHandler();
|
||||
}
|
||||
servedContext = new ControlledInputConnectionWrapper(
|
||||
icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this);
|
||||
} else {
|
||||
servedContext = null;
|
||||
missingMethodFlags = 0;
|
||||
}
|
||||
if (mServedInputConnectionWrapper != null) {
|
||||
mServedInputConnectionWrapper.deactivate();
|
||||
@@ -1248,7 +1258,7 @@ public final class InputMethodManager {
|
||||
+ Integer.toHexString(controlFlags));
|
||||
final InputBindResult res = mService.startInputOrWindowGainedFocus(
|
||||
startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,
|
||||
windowFlags, tba, servedContext);
|
||||
windowFlags, tba, servedContext, missingMethodFlags);
|
||||
if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
|
||||
if (res != null) {
|
||||
if (res.id != null) {
|
||||
@@ -1476,7 +1486,7 @@ public final class InputMethodManager {
|
||||
mService.startInputOrWindowGainedFocus(
|
||||
InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN_REPORT_ONLY, mClient,
|
||||
rootView.getWindowToken(), controlFlags, softInputMode, windowFlags, null,
|
||||
null);
|
||||
null, 0 /* missingMethodFlags */);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ oneway interface IInputMethod {
|
||||
|
||||
void unbindInput();
|
||||
|
||||
void startInput(in IInputContext inputContext, in EditorInfo attribute);
|
||||
void startInput(in IInputContext inputContext, int missingMethods, in EditorInfo attribute);
|
||||
|
||||
void restartInput(in IInputContext inputContext, in EditorInfo attribute);
|
||||
void restartInput(in IInputContext inputContext, int missingMethods, in EditorInfo attribute);
|
||||
|
||||
void createSession(in InputChannel channel, IInputSessionCallback callback);
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ interface IInputMethodManager {
|
||||
/* @InputMethodClient.StartInputReason */ int startInputReason,
|
||||
in IInputMethodClient client, in IBinder windowToken, int controlFlags,
|
||||
int softInputMode, int windowFlags, in EditorInfo attribute,
|
||||
IInputContext inputContext);
|
||||
IInputContext inputContext,
|
||||
/* @InputConnectionInspector.MissingMethodFlags */ int missingMethodFlags);
|
||||
|
||||
void showInputMethodPickerFromClient(in IInputMethodClient client,
|
||||
int auxiliarySubtypeMode);
|
||||
|
||||
@@ -27,11 +27,15 @@ import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputConnectionInspector;
|
||||
import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
|
||||
|
||||
public class InputConnectionWrapper implements InputConnection {
|
||||
private static final int MAX_WAIT_TIME_MILLIS = 2000;
|
||||
private final IInputContext mIInputContext;
|
||||
|
||||
@MissingMethodFlags
|
||||
private final int mMissingMethods;
|
||||
|
||||
static class InputContextCallback extends IInputContextCallback.Stub {
|
||||
private static final String TAG = "InputConnectionWrapper.ICC";
|
||||
public int mSeq;
|
||||
@@ -191,8 +195,10 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
}
|
||||
}
|
||||
|
||||
public InputConnectionWrapper(IInputContext inputContext) {
|
||||
public InputConnectionWrapper(IInputContext inputContext,
|
||||
@MissingMethodFlags final int missingMethods) {
|
||||
mIInputContext = inputContext;
|
||||
mMissingMethods = missingMethods;
|
||||
}
|
||||
|
||||
public CharSequence getTextAfterCursor(int length, int flags) {
|
||||
@@ -230,8 +236,12 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public CharSequence getSelectedText(int flags) {
|
||||
if (isMethodMissing(MissingMethodFlags.GET_SELECTED_TEXT)) {
|
||||
// This method is not implemented.
|
||||
return null;
|
||||
}
|
||||
CharSequence value = null;
|
||||
try {
|
||||
InputContextCallback callback = InputContextCallback.getInstance();
|
||||
@@ -295,6 +305,10 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
}
|
||||
|
||||
public boolean commitCompletion(CompletionInfo text) {
|
||||
if (isMethodMissing(MissingMethodFlags.COMMIT_CORRECTION)) {
|
||||
// This method is not implemented.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
mIInputContext.commitCompletion(text);
|
||||
return true;
|
||||
@@ -340,6 +354,10 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
}
|
||||
|
||||
public boolean setComposingRegion(int start, int end) {
|
||||
if (isMethodMissing(MissingMethodFlags.SET_COMPOSING_REGION)) {
|
||||
// This method is not implemented.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
mIInputContext.setComposingRegion(start, end);
|
||||
return true;
|
||||
@@ -412,6 +430,10 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
}
|
||||
|
||||
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
|
||||
if (isMethodMissing(MissingMethodFlags.DELETE_SURROUNDING_TEXT_IN_CODE_POINTS)) {
|
||||
// This method is not implemented.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
mIInputContext.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
|
||||
return true;
|
||||
@@ -440,6 +462,10 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
|
||||
public boolean requestCursorUpdates(int cursorUpdateMode) {
|
||||
boolean result = false;
|
||||
if (isMethodMissing(MissingMethodFlags.REQUEST_CURSOR_UPDATES)) {
|
||||
// This method is not implemented.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
InputContextCallback callback = InputContextCallback.getInstance();
|
||||
mIInputContext.requestUpdateCursorAnchorInfo(cursorUpdateMode, callback.mSeq, callback);
|
||||
@@ -460,4 +486,16 @@ public class InputConnectionWrapper implements InputConnection {
|
||||
// Nothing should happen when called from input method.
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
|
||||
return (mMissingMethods & methodFlag) == methodFlag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InputConnectionWrapper{idHash=#"
|
||||
+ Integer.toHexString(System.identityHashCode(this))
|
||||
+ " mMissingMethods="
|
||||
+ InputConnectionInspector.getMissingMethodFlagsAsString(mMissingMethods) + "}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ import android.view.WindowManager;
|
||||
import android.view.WindowManagerInternal;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputBinding;
|
||||
import android.view.inputmethod.InputConnectionInspector;
|
||||
import android.view.inputmethod.InputMethod;
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
@@ -326,6 +327,13 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
*/
|
||||
IInputContext mCurInputContext;
|
||||
|
||||
/**
|
||||
* The missing method flags for the input context last provided by the current client.
|
||||
*
|
||||
* @see android.view.inputmethod.InputConnectionInspector.MissingMethodFlags
|
||||
*/
|
||||
int mCurInputContextMissingMethods;
|
||||
|
||||
/**
|
||||
* The attributes last provided by the current client.
|
||||
*/
|
||||
@@ -1288,11 +1296,13 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
}
|
||||
final SessionState session = mCurClient.curSession;
|
||||
if (initial) {
|
||||
executeOrSendMessage(session.method, mCaller.obtainMessageOOO(
|
||||
MSG_START_INPUT, session, mCurInputContext, mCurAttribute));
|
||||
executeOrSendMessage(session.method, mCaller.obtainMessageIOOO(
|
||||
MSG_START_INPUT, mCurInputContextMissingMethods, session, mCurInputContext,
|
||||
mCurAttribute));
|
||||
} else {
|
||||
executeOrSendMessage(session.method, mCaller.obtainMessageOOO(
|
||||
MSG_RESTART_INPUT, session, mCurInputContext, mCurAttribute));
|
||||
executeOrSendMessage(session.method, mCaller.obtainMessageIOOO(
|
||||
MSG_RESTART_INPUT, mCurInputContextMissingMethods, session, mCurInputContext,
|
||||
mCurAttribute));
|
||||
}
|
||||
if (mShowRequested) {
|
||||
if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
|
||||
@@ -1305,7 +1315,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
|
||||
InputBindResult startInputLocked(
|
||||
/* @InputMethodClient.StartInputReason */ final int startInputReason,
|
||||
IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
|
||||
IInputMethodClient client, IInputContext inputContext,
|
||||
/* @InputConnectionInspector.missingMethods */ final int missingMethods,
|
||||
EditorInfo attribute,
|
||||
int controlFlags) {
|
||||
// If no method is currently selected, do nothing.
|
||||
if (mCurMethodId == null) {
|
||||
@@ -1332,10 +1344,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
} catch (RemoteException e) {
|
||||
}
|
||||
|
||||
return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags);
|
||||
return startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
|
||||
controlFlags);
|
||||
}
|
||||
|
||||
InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
|
||||
/* @InputConnectionInspector.missingMethods */ final int missingMethods,
|
||||
@NonNull EditorInfo attribute, int controlFlags) {
|
||||
// If no method is currently selected, do nothing.
|
||||
if (mCurMethodId == null) {
|
||||
@@ -1370,6 +1384,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
if (mCurSeq <= 0) mCurSeq = 1;
|
||||
mCurClient = cs;
|
||||
mCurInputContext = inputContext;
|
||||
mCurInputContextMissingMethods = missingMethods;
|
||||
mCurAttribute = attribute;
|
||||
|
||||
// Check if the input method is changing.
|
||||
@@ -1458,8 +1473,9 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
|
||||
private InputBindResult startInput(
|
||||
/* @InputMethodClient.StartInputReason */ final int startInputReason,
|
||||
IInputMethodClient client, IInputContext inputContext, EditorInfo attribute,
|
||||
int controlFlags) {
|
||||
IInputMethodClient client, IInputContext inputContext,
|
||||
/* @InputConnectionInspector.missingMethods */ final int missingMethods,
|
||||
EditorInfo attribute, int controlFlags) {
|
||||
if (!calledFromValidUser()) {
|
||||
return null;
|
||||
}
|
||||
@@ -1469,13 +1485,15 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
+ InputMethodClient.getStartInputReason(startInputReason)
|
||||
+ " client = " + client.asBinder()
|
||||
+ " inputContext=" + inputContext
|
||||
+ " missingMethods="
|
||||
+ InputConnectionInspector.getMissingMethodFlagsAsString(missingMethods)
|
||||
+ " attribute=" + attribute
|
||||
+ " controlFlags=#" + Integer.toHexString(controlFlags));
|
||||
}
|
||||
final long ident = Binder.clearCallingIdentity();
|
||||
try {
|
||||
return startInputLocked(startInputReason, client, inputContext, attribute,
|
||||
controlFlags);
|
||||
return startInputLocked(startInputReason, client, inputContext, missingMethods,
|
||||
attribute, controlFlags);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(ident);
|
||||
}
|
||||
@@ -2189,19 +2207,22 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
public InputBindResult startInputOrWindowGainedFocus(
|
||||
/* @InputMethodClient.StartInputReason */ final int startInputReason,
|
||||
IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
|
||||
int windowFlags, EditorInfo attribute, IInputContext inputContext) {
|
||||
int windowFlags, EditorInfo attribute, IInputContext inputContext,
|
||||
/* @InputConnectionInspector.missingMethods */ final int missingMethods) {
|
||||
if (windowToken != null) {
|
||||
return windowGainedFocus(startInputReason, client, windowToken, controlFlags,
|
||||
softInputMode, windowFlags, attribute, inputContext);
|
||||
softInputMode, windowFlags, attribute, inputContext, missingMethods);
|
||||
} else {
|
||||
return startInput(startInputReason, client, inputContext, attribute, controlFlags);
|
||||
return startInput(startInputReason, client, inputContext, missingMethods, attribute,
|
||||
controlFlags);
|
||||
}
|
||||
}
|
||||
|
||||
private InputBindResult windowGainedFocus(
|
||||
/* @InputMethodClient.StartInputReason */ final int startInputReason,
|
||||
IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
|
||||
int windowFlags, EditorInfo attribute, IInputContext inputContext) {
|
||||
int windowFlags, EditorInfo attribute, IInputContext inputContext,
|
||||
/* @InputConnectionInspector.missingMethods */ final int missingMethods) {
|
||||
// Needs to check the validity before clearing calling identity
|
||||
final boolean calledFromValidUser = calledFromValidUser();
|
||||
InputBindResult res = null;
|
||||
@@ -2212,6 +2233,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
+ InputMethodClient.getStartInputReason(startInputReason)
|
||||
+ " client=" + client.asBinder()
|
||||
+ " inputContext=" + inputContext
|
||||
+ " missingMethods="
|
||||
+ InputConnectionInspector.getMissingMethodFlagsAsString(missingMethods)
|
||||
+ " attribute=" + attribute
|
||||
+ " controlFlags=#" + Integer.toHexString(controlFlags)
|
||||
+ " softInputMode=#" + Integer.toHexString(softInputMode)
|
||||
@@ -2249,8 +2272,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
|
||||
+ " attribute=" + attribute + ", token = " + windowToken);
|
||||
if (attribute != null) {
|
||||
return startInputUncheckedLocked(cs, inputContext, attribute,
|
||||
controlFlags);
|
||||
return startInputUncheckedLocked(cs, inputContext, missingMethods,
|
||||
attribute, controlFlags);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -2299,8 +2322,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
// is more room for the target window + IME.
|
||||
if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
|
||||
if (attribute != null) {
|
||||
res = startInputUncheckedLocked(cs, inputContext, attribute,
|
||||
controlFlags);
|
||||
res = startInputUncheckedLocked(cs, inputContext,
|
||||
missingMethods, attribute, controlFlags);
|
||||
didStart = true;
|
||||
}
|
||||
showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
|
||||
@@ -2325,8 +2348,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
|
||||
if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
|
||||
if (attribute != null) {
|
||||
res = startInputUncheckedLocked(cs, inputContext, attribute,
|
||||
controlFlags);
|
||||
res = startInputUncheckedLocked(cs, inputContext,
|
||||
missingMethods, attribute, controlFlags);
|
||||
didStart = true;
|
||||
}
|
||||
showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
|
||||
@@ -2335,8 +2358,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
|
||||
if (DEBUG) Slog.v(TAG, "Window asks to always show input");
|
||||
if (attribute != null) {
|
||||
res = startInputUncheckedLocked(cs, inputContext, attribute,
|
||||
controlFlags);
|
||||
res = startInputUncheckedLocked(cs, inputContext, missingMethods,
|
||||
attribute, controlFlags);
|
||||
didStart = true;
|
||||
}
|
||||
showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
|
||||
@@ -2344,7 +2367,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
}
|
||||
|
||||
if (!didStart && attribute != null) {
|
||||
res = startInputUncheckedLocked(cs, inputContext, attribute,
|
||||
res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
|
||||
controlFlags);
|
||||
}
|
||||
}
|
||||
@@ -2806,28 +2829,32 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
|
||||
}
|
||||
// ---------------------------------------------------------
|
||||
|
||||
case MSG_START_INPUT:
|
||||
args = (SomeArgs)msg.obj;
|
||||
case MSG_START_INPUT: {
|
||||
int missingMethods = msg.arg1;
|
||||
args = (SomeArgs) msg.obj;
|
||||
try {
|
||||
SessionState session = (SessionState)args.arg1;
|
||||
SessionState session = (SessionState) args.arg1;
|
||||
setEnabledSessionInMainThread(session);
|
||||
session.method.startInput((IInputContext)args.arg2,
|
||||
(EditorInfo)args.arg3);
|
||||
session.method.startInput((IInputContext) args.arg2, missingMethods,
|
||||
(EditorInfo) args.arg3);
|
||||
} catch (RemoteException e) {
|
||||
}
|
||||
args.recycle();
|
||||
return true;
|
||||
case MSG_RESTART_INPUT:
|
||||
args = (SomeArgs)msg.obj;
|
||||
}
|
||||
case MSG_RESTART_INPUT: {
|
||||
int missingMethods = msg.arg1;
|
||||
args = (SomeArgs) msg.obj;
|
||||
try {
|
||||
SessionState session = (SessionState)args.arg1;
|
||||
SessionState session = (SessionState) args.arg1;
|
||||
setEnabledSessionInMainThread(session);
|
||||
session.method.restartInput((IInputContext)args.arg2,
|
||||
(EditorInfo)args.arg3);
|
||||
session.method.restartInput((IInputContext) args.arg2, missingMethods,
|
||||
(EditorInfo) args.arg3);
|
||||
} catch (RemoteException e) {
|
||||
}
|
||||
args.recycle();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
|
||||
|
||||
@@ -222,7 +222,8 @@ public class BridgeIInputMethodManager implements IInputMethodManager {
|
||||
public InputBindResult startInputOrWindowGainedFocus(
|
||||
/* @InputMethodClient.StartInputReason */ int startInputReason,
|
||||
IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,
|
||||
int windowFlags, EditorInfo attribute, IInputContext inputContext)
|
||||
int windowFlags, EditorInfo attribute, IInputContext inputContext,
|
||||
/* @InputConnectionInspector.MissingMethodFlags */ int missingMethodFlags)
|
||||
throws RemoteException {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user