From: Yohei Yukawa Date: Mon, 13 Feb 2017 20:04:41 +0000 (-0800) Subject: Track event flow to IMMS#setImeWindowStatus X-Git-Tag: android-x86-8.1-r1~3877^2~1 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=6db3bfe33d92127d203ec872a0b353585a99f256;p=android-x86%2Fframeworks-base.git Track event flow to IMMS#setImeWindowStatus This is part of work to introduce historical debugging infrastructure for Android IME. In this CL, we will focus on the following two event flows. A1. IMMS#attachNewInputLocked() queues MSG_(RE)START_INPUT to deliver new InputConnection/EditorInfo to the current IME A2. The IME triggers IMS#onStartInput()/IMS#onRestartInput() and updates the following fields: - InputMethodService#mStartedInputConnection - InputMethodService#mInputEditorInfo B1. IME is expected to call back IMM#setImeWindowStatus() to notify its window visibility change to IMMS. B2. IMMS updates the following field if the caller is still the current IME. - InputMethodManagerService#mImeWindowVis What this CL aims to do is to enable IMMS to access A1 state when it was in B2 state, by considering that for given a B1 the last A2 happened before B1 is the cause of B1 and B2. To do this, IMMS issues a binder token in A1 and each IME keeps it so that it can be passed in B1. By using this Binder token as a key, IMMS can keep tracking state snapshot taken from each A1. Note that those state snapshots keep alive until the Binder token's proxy in the IME process loses strong reference from its GC root. Test: Make sure `adb shell dumpsys input_method | grep mImeWindowVis` matches to the IME window visibility. Test: Make sure the current IME is not receiving any InvalidParameterException from IMMS. Bug: 35079353 Change-Id: I9921b381e02106dbffff5e0b3d13f0a1245ce807 --- diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index bc407504a20d..562f40bc7513 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -166,8 +166,9 @@ class IInputMethodWrapper extends IInputMethod.Stub final SomeArgs args = (SomeArgs) msg.obj; final int missingMethods = msg.arg1; final boolean restarting = msg.arg2 != 0; - final IInputContext inputContext = (IInputContext) args.arg1; - final EditorInfo info = (EditorInfo) args.arg2; + final IBinder startInputToken = (IBinder) args.arg1; + final IInputContext inputContext = (IInputContext) args.arg2; + final EditorInfo info = (EditorInfo) args.arg3; final InputConnection ic = inputContext != null ? new InputConnectionWrapper(mTarget, inputContext, missingMethods) : null; info.makeCompatible(mTargetSdkVersion); @@ -176,6 +177,8 @@ class IInputMethodWrapper extends IInputMethod.Stub } else { inputMethod.startInput(ic, info); } + inputMethod.dispatchStartInputWithToken(ic, info, true /* initial */, + startInputToken); args.recycle(); return; } @@ -255,11 +258,11 @@ class IInputMethodWrapper extends IInputMethod.Stub } @Override - public void startInput(IInputContext inputContext, + public void startInput(IBinder startInputToken, IInputContext inputContext, @InputConnectionInspector.MissingMethodFlags final int missingMethods, EditorInfo attribute, boolean restarting) { - mCaller.executeOrSendMessage(mCaller.obtainMessageIIOO(DO_START_INPUT, - missingMethods, restarting ? 1 : 0, inputContext, attribute)); + mCaller.executeOrSendMessage(mCaller.obtainMessageIIOOO(DO_START_INPUT, + missingMethods, restarting ? 1 : 0, startInputToken, inputContext, attribute)); } @Override diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 5ae1fd057122..7a20943e2a4b 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -24,6 +24,7 @@ import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.Dialog; import android.content.Context; @@ -291,7 +292,20 @@ public class InputMethodService extends AbstractInputMethodService { boolean mCandidatesViewStarted; InputConnection mStartedInputConnection; EditorInfo mInputEditorInfo; - + + /** + * A token to keep tracking the last IPC that triggered + * {@link #doStartInput(InputConnection, EditorInfo, boolean)}. If + * {@link #doStartInput(InputConnection, EditorInfo, boolean)} was not caused by IPCs from + * {@link com.android.server.InputMethodManagerService}, this needs to remain unchanged. + * + *

Some IPCs to {@link com.android.server.InputMethodManagerService} require this token to + * disentangle event flows for various purposes such as better window animation and providing + * fine-grained debugging information.

+ */ + @Nullable + private IBinder mStartInputToken; + int mShowInputFlags; boolean mShowInputRequested; boolean mLastShowInputRequested; @@ -416,6 +430,23 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * {@inheritDoc} + * @hide + */ + @Override + public void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, + @NonNull EditorInfo editorInfo, boolean restarting, + @NonNull IBinder startInputToken) { + mStartInputToken = startInputToken; + + // This needs to be dispatched to interface methods rather than doStartInput(). + // Otherwise IME developers who have overridden those interface methods will lose + // notifications. + super.dispatchStartInputWithToken(inputConnection, editorInfo, restarting, + startInputToken); + } + + /** * Handle a request by the system to hide the soft input area. */ public void hideSoftInput(int flags, ResultReceiver resultReceiver) { @@ -454,8 +485,8 @@ public class InputMethodService extends AbstractInputMethodService { clearInsetOfPreviousIme(); // If user uses hard keyboard, IME button should always be shown. boolean showing = isInputViewShown(); - mImm.setImeWindowStatus(mToken, IME_ACTIVE | (showing ? IME_VISIBLE : 0), - mBackDisposition); + mImm.setImeWindowStatus(mToken, mStartInputToken, + IME_ACTIVE | (showing ? IME_VISIBLE : 0), mBackDisposition); if (resultReceiver != null) { resultReceiver.send(wasVis != isInputViewShown() ? InputMethodManager.RESULT_SHOWN @@ -926,8 +957,8 @@ public class InputMethodService extends AbstractInputMethodService { } // If user uses hard keyboard, IME button should always be shown. boolean showing = onEvaluateInputViewShown(); - mImm.setImeWindowStatus(mToken, IME_ACTIVE | (showing ? IME_VISIBLE : 0), - mBackDisposition); + mImm.setImeWindowStatus(mToken, mStartInputToken, + IME_ACTIVE | (showing ? IME_VISIBLE : 0), mBackDisposition); } } @@ -1653,7 +1684,8 @@ public class InputMethodService extends AbstractInputMethodService { final int nextImeWindowStatus = IME_ACTIVE | (isInputViewShown() ? IME_VISIBLE : 0); if (previousImeWindowStatus != nextImeWindowStatus) { - mImm.setImeWindowStatus(mToken, nextImeWindowStatus, mBackDisposition); + mImm.setImeWindowStatus(mToken, mStartInputToken, nextImeWindowStatus, + mBackDisposition); } if ((previousImeWindowStatus & IME_ACTIVE) == 0) { if (DEBUG) Log.v(TAG, "showWindow: showing!"); @@ -1678,7 +1710,7 @@ public class InputMethodService extends AbstractInputMethodService { } private void doHideWindow() { - mImm.setImeWindowStatus(mToken, 0, mBackDisposition); + mImm.setImeWindowStatus(mToken, mStartInputToken, 0, mBackDisposition); hideWindow(); } @@ -2643,7 +2675,8 @@ public class InputMethodService extends AbstractInputMethodService { p.println(" mInputStarted=" + mInputStarted + " mInputViewStarted=" + mInputViewStarted + " mCandidatesViewStarted=" + mCandidatesViewStarted); - + p.println(" mStartInputToken=" + mStartInputToken); + if (mInputEditorInfo != null) { p.println(" mInputEditorInfo:"); mInputEditorInfo.dump(p, " "); diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java index 5ab3d34a8b3d..0922422c5125 100644 --- a/core/java/android/view/inputmethod/InputMethod.java +++ b/core/java/android/view/inputmethod/InputMethod.java @@ -16,6 +16,8 @@ package android.view.inputmethod; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.inputmethodservice.InputMethodService; @@ -148,6 +150,44 @@ public interface InputMethod { public void restartInput(InputConnection inputConnection, EditorInfo attribute); /** + * This method is called when {@code {@link #startInput(InputConnection, EditorInfo)} or + * {@code {@link #restartInput(InputConnection, EditorInfo)} needs to be dispatched. + * + *

Note: This method is hidden because the {@code startInputToken} that this method is + * dealing with is one of internal details, which should not be exposed to the IME developers. + * If you override this method, you are responsible for not breaking existing IMEs that expect + * {@link #startInput(InputConnection, EditorInfo)} to be still called back.

+ * + * @param inputConnection optional specific input connection for communicating with the text + * box; if {@code null}, you should use the generic bound input + * connection + * @param editorInfo information about the text box (typically, an EditText) that requests input + * @param restarting {@code false} if this corresponds to + * {@link #startInput(InputConnection, EditorInfo)}. Otherwise this + * corresponds to {@link #restartInput(InputConnection, EditorInfo)}. + * @param startInputToken a token that identifies a logical session that starts with this method + * call. Some internal IPCs such as {@link + * InputMethodManager#setImeWindowStatus(IBinder, IBinder, int, int)} + * require this token to work, and you have to keep the token alive until + * the next {@link #startInput(InputConnection, EditorInfo, IBinder)} as + * long as your implementation of {@link InputMethod} relies on such + * IPCs + * @see #startInput(InputConnection, EditorInfo) + * @see #restartInput(InputConnection, EditorInfo) + * @see EditorInfo + * @hide + */ + default void dispatchStartInputWithToken(@Nullable InputConnection inputConnection, + @NonNull EditorInfo editorInfo, boolean restarting, + @NonNull IBinder startInputToken) { + if (restarting) { + restartInput(inputConnection, editorInfo); + } else { + startInput(inputConnection, editorInfo); + } + } + + /** * Create a new {@link InputMethodSession} that can be handed to client * applications for interacting with the input method. You can later * use {@link #revokeSession(InputMethodSession)} to destroy the session diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 13abe7c6471d..80e9aa4fff31 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -738,9 +738,10 @@ public final class InputMethodManager { } /** @hide */ - public void setImeWindowStatus(IBinder imeToken, int vis, int backDisposition) { + public void setImeWindowStatus(IBinder imeToken, IBinder startInputToken, int vis, + int backDisposition) { try { - mService.setImeWindowStatus(imeToken, vis, backDisposition); + mService.setImeWindowStatus(imeToken, startInputToken, vis, backDisposition); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java index c26fc3a0a713..ae7c5f230e22 100644 --- a/core/java/com/android/internal/os/HandlerCaller.java +++ b/core/java/com/android/internal/os/HandlerCaller.java @@ -173,6 +173,16 @@ public class HandlerCaller { return mH.obtainMessage(what, arg1, arg2, args); } + public Message obtainMessageIIOOOO(int what, int arg1, int arg2, Object arg3, Object arg4, + Object arg5, Object arg6) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = arg3; + args.arg2 = arg4; + args.arg3 = arg5; + args.arg4 = arg6; + return mH.obtainMessage(what, arg1, arg2, args); + } + public Message obtainMessageOO(int what, Object arg1, Object arg2) { SomeArgs args = SomeArgs.obtain(); args.arg1 = arg1; diff --git a/core/java/com/android/internal/view/IInputMethod.aidl b/core/java/com/android/internal/view/IInputMethod.aidl index 9870612dfac5..5b29f4cb9fb4 100644 --- a/core/java/com/android/internal/view/IInputMethod.aidl +++ b/core/java/com/android/internal/view/IInputMethod.aidl @@ -38,8 +38,8 @@ oneway interface IInputMethod { void unbindInput(); - void startInput(in IInputContext inputContext, int missingMethods, in EditorInfo attribute, - boolean restarting); + void startInput(in IBinder startInputToken, in IInputContext inputContext, int missingMethods, + in EditorInfo attribute, boolean restarting); void createSession(in InputChannel channel, IInputSessionCallback callback); diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 2279a6706524..f42eab6e6622 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -70,7 +70,8 @@ interface IInputMethodManager { void hideMySoftInput(in IBinder token, int flags); void showMySoftInput(in IBinder token, int flags); void updateStatusIcon(in IBinder token, String packageName, int iconId); - void setImeWindowStatus(in IBinder token, int vis, int backDisposition); + void setImeWindowStatus(in IBinder token, in IBinder startInputToken, int vis, + int backDisposition); void registerSuggestionSpansForNotification(in SuggestionSpan[] spans); boolean notifySuggestionPicked(in SuggestionSpan span, String originalString, int index); InputMethodSubtype getCurrentInputMethodSubtype(); diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java index b9c47e759bda..b6a0e5cc9957 100644 --- a/services/core/java/com/android/server/InputMethodManagerService.java +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -124,6 +124,7 @@ import android.view.WindowManager; import android.view.WindowManagerInternal; 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.InputMethodInfo; @@ -152,6 +153,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.WeakHashMap; /** * This class provides a system service that manages input methods. @@ -499,6 +501,36 @@ public class InputMethodManagerService extends IInputMethodManager.Stub @HardKeyboardBehavior private final int mHardKeyboardBehavior; + /** + * Internal state snapshot when {@link #MSG_START_INPUT} message is about to be posted to the + * internal message queue. Any subsequent state change inside {@link InputMethodManagerService} + * will not affect those tasks that are already posted. + * + *

Posting {@link #MSG_START_INPUT} message basically means that + * {@link InputMethodService#doStartInput(InputConnection, EditorInfo, boolean)} will be called + * back in the current IME process shortly, which will also affect what the current IME starts + * receiving from {@link InputMethodService#getCurrentInputConnection()}. In other words, this + * snapshot will be taken every time when {@link InputMethodManagerService} is initiating a new + * logical input session between the client application and the current IME.

+ * + *

Be careful to not keep strong references to this object forever, which can prevent + * {@link StartInputInfo#mImeToken} and {@link StartInputInfo#mTargetWindow} from being GC-ed. + *

+ */ + private static class StartInputInfo { + @NonNull + final IBinder mImeToken; + @Nullable + final IBinder mTargetWindow; + + StartInputInfo(@NonNull IBinder imeToken, @Nullable IBinder targetWindow) { + mImeToken = imeToken; + mTargetWindow = targetWindow; + } + } + + private WeakHashMap mStartInputMap = new WeakHashMap<>(); + class SettingsObserver extends ContentObserver { int mUserId; boolean mRegistered = false; @@ -1338,10 +1370,15 @@ public class InputMethodManagerService extends IInputMethodManager.Stub MSG_BIND_INPUT, mCurMethod, mCurClient.binding)); mBoundToMethod = true; } + + final Binder startInputToken = new Binder(); + final StartInputInfo info = new StartInputInfo(mCurToken, mCurFocusedWindow); + mStartInputMap.put(startInputToken, info); + final SessionState session = mCurClient.curSession; - executeOrSendMessage(session.method, mCaller.obtainMessageIIOOO( + executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO( MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */, - session, mCurInputContext, mCurAttribute)); + startInputToken, session, mCurInputContext, mCurAttribute)); if (mShowRequested) { if (DEBUG) Slog.v(TAG, "Attach new input asks to show input"); showCurrentInputLocked(getAppShowFlags(), null); @@ -1809,12 +1846,21 @@ public class InputMethodManagerService extends IInputMethodManager.Stub @SuppressWarnings("deprecation") @Override - public void setImeWindowStatus(IBinder token, int vis, int backDisposition) { + public void setImeWindowStatus(IBinder token, IBinder startInputToken, int vis, + int backDisposition) { + if (startInputToken == null) { + throw new InvalidParameterException("startInputToken cannot be null"); + } + if (!calledWithValidToken(token)) { return; } synchronized (mMethodMap) { + final StartInputInfo info = mStartInputMap.get(startInputToken); + if (info == null) { + throw new InvalidParameterException("Unknown startInputToken=" + startInputToken); + } mImeWindowVis = vis; mBackDisposition = backDisposition; updateSystemUiLocked(token, vis, backDisposition); @@ -2892,12 +2938,14 @@ public class InputMethodManagerService extends IInputMethodManager.Stub final int missingMethods = msg.arg1; final boolean restarting = msg.arg2 != 0; args = (SomeArgs) msg.obj; - final SessionState session = (SessionState) args.arg1; - final IInputContext inputContext = (IInputContext) args.arg2; - final EditorInfo editorInfo = (EditorInfo) args.arg3; + final IBinder startInputToken = (IBinder) args.arg1; + final SessionState session = (SessionState) args.arg2; + final IInputContext inputContext = (IInputContext) args.arg3; + final EditorInfo editorInfo = (EditorInfo) args.arg4; try { setEnabledSessionInMainThread(session); - session.method.startInput(inputContext, missingMethods, editorInfo, restarting); + session.method.startInput(startInputToken, inputContext, missingMethods, + editorInfo, restarting); } catch (RemoteException e) { } args.recycle(); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java index 468710bbb470..0ab6c232757b 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java @@ -135,7 +135,8 @@ public class BridgeIInputMethodManager implements IInputMethodManager { } @Override - public void setImeWindowStatus(IBinder arg0, int arg1, int arg2) throws RemoteException { + public void setImeWindowStatus(IBinder arg0, IBinder arg1, int arg2, int arg3) + throws RemoteException { // TODO Auto-generated method stub }