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/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
index 9b8f2d2..68d9988 100644
--- a/services/core/java/com/android/server/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -112,6 +112,7 @@
 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;
@@ -327,6 +328,13 @@
     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.
      */
     EditorInfo mCurAttribute;
@@ -1288,11 +1296,13 @@
         }
         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 @@
 
     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 @@
         } 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 @@
         if (mCurSeq <= 0) mCurSeq = 1;
         mCurClient = cs;
         mCurInputContext = inputContext;
+        mCurInputContextMissingMethods = missingMethods;
         mCurAttribute = attribute;
 
         // Check if the input method is changing.
@@ -1458,8 +1473,9 @@
 
     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 @@
                         + 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 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 @@
                         + 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 @@
                     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 @@
                             // 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 @@
                                 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 @@
                     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 @@
                 }
 
                 if (!didStart && attribute != null) {
-                    res = startInputUncheckedLocked(cs, inputContext, attribute,
+                    res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
                             controlFlags);
                 }
             }
@@ -2806,28 +2829,32 @@
             }
             // ---------------------------------------------------------
 
-            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;
+            }
 
             // ---------------------------------------------------------