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
diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java
index fc67245..85b8606 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -27,11 +27,15 @@
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 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 @@
}
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 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 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 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 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 @@
// 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) + "}";
+ }
}