diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 42acc2214ff10..c1cbeef0bba99 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3256,6 +3256,7 @@ package android.view.inputmethod { method public int getDisplayId(); method @NonNull @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public java.util.List getInputMethodListAsUser(int); method public boolean hasActiveInputConnection(@Nullable android.view.View); + method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public boolean hasPendingImeVisibilityRequests(); method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShown(); method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void setStylusWindowIdleTimeoutForTest(long); field public static final long CLEAR_SHOW_FORCED_FLAG_WHEN_LEAVING = 214016041L; // 0xcc1a029L diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index b8cd7b9d177a5..f87b746fa72e6 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -16,6 +16,7 @@ package android.view; +import static android.os.Trace.TRACE_TAG_VIEW; import static android.view.ImeInsetsSourceConsumerProto.INSETS_SOURCE_CONSUMER; import static android.view.ImeInsetsSourceConsumerProto.IS_HIDE_ANIMATION_RUNNING; import static android.view.ImeInsetsSourceConsumerProto.IS_REQUESTED_VISIBLE_AWAITING_CONTROL; @@ -23,11 +24,15 @@ import static android.view.ImeInsetsSourceConsumerProto.IS_SHOW_REQUESTED_DURING import android.annotation.Nullable; import android.os.IBinder; +import android.os.Process; +import android.os.Trace; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl.Transaction; +import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputMethodManager; import com.android.internal.inputmethod.ImeTracing; +import com.android.internal.inputmethod.SoftInputShowHideReason; import java.util.function.Supplier; @@ -48,8 +53,8 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { /** * Tracks whether {@link WindowInsetsController#show(int)} or * {@link InputMethodManager#showSoftInput(View, int)} is called during IME hide animation. - * If it was called, we should not call {@link InputMethodManager#notifyImeHidden(IBinder)}, - * because the IME is being shown. + * If it was called, we should not call {@link InputMethodManager#notifyImeHidden(IBinder, + * ImeTracker.Token)}, because the IME is being shown. */ private boolean mIsShowRequestedDuringHideAnimation; @@ -76,7 +81,7 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { // Remove IME surface as IME has finished hide animation, if there is no pending // show request. if (!mIsShowRequestedDuringHideAnimation) { - notifyHidden(); + notifyHidden(null /* statsToken */); removeSurface(); } } @@ -120,7 +125,8 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { * @return @see {@link android.view.InsetsSourceConsumer.ShowResult}. */ @Override - public @ShowResult int requestShow(boolean fromIme) { + @ShowResult + public int requestShow(boolean fromIme, @Nullable ImeTracker.Token statsToken) { if (fromIme) { ImeTracing.getInstance().triggerClientDump( "ImeInsetsSourceConsumer#requestShow", @@ -129,6 +135,9 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { // TODO: ResultReceiver for IME. // TODO: Set mShowOnNextImeRender to automatically show IME and guard it with a flag. + ImeTracker.get().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW); + if (getControl() == null) { // If control is null, schedule to show IME when control is available. mIsRequestedVisibleAwaitingControl = true; @@ -140,16 +149,32 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { return ShowResult.SHOW_IMMEDIATELY; } - return getImm().requestImeShow(mController.getHost().getWindowToken()) + return getImm().requestImeShow(mController.getHost().getWindowToken(), statsToken) ? ShowResult.IME_SHOW_DELAYED : ShowResult.IME_SHOW_FAILED; } /** * Notify {@link com.android.server.inputmethod.InputMethodManagerService} that * IME insets are hidden. + * + * @param statsToken the token tracking the current IME hide request or {@code null} otherwise. */ - private void notifyHidden() { - getImm().notifyImeHidden(mController.getHost().getWindowToken()); + private void notifyHidden(@Nullable ImeTracker.Token statsToken) { + // Create a new stats token to track the hide request when: + // - we do not already have one, or + // - we do already have one, but we have control and use the passed in token + // for the insets animation already. + if (statsToken == null || getControl() != null) { + statsToken = ImeTracker.get().onRequestHide(null /* component */, Process.myUid(), + ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, + SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); + } + + ImeTracker.get().onProgress(statsToken, + ImeTracker.PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN); + + getImm().notifyImeHidden(mController.getHost().getWindowToken(), statsToken); + Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.hideRequestFromApi", 0); } @Override diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 709bc2b649df4..8abe66afae4d5 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -41,6 +41,7 @@ import android.graphics.Rect; import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; +import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.ArraySet; @@ -64,6 +65,7 @@ import android.view.inputmethod.InputMethodManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.internal.inputmethod.ImeTracing; +import com.android.internal.inputmethod.SoftInputShowHideReason; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -977,7 +979,14 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public void show(@InsetsType int types) { - show(types, false /* fromIme */, null /* statsToken */); + ImeTracker.Token statsToken = null; + if ((types & ime()) != 0) { + statsToken = ImeTracker.get().onRequestShow(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, + SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API); + } + + show(types, false /* fromIme */, statsToken); } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @@ -1054,7 +1063,14 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public void hide(@InsetsType int types) { - hide(types, false /* fromIme */, null /* statsToken */); + ImeTracker.Token statsToken = null; + if ((types & ime()) != 0) { + statsToken = ImeTracker.get().onRequestHide(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, + SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); + } + + hide(types, false /* fromIme */, statsToken); } @VisibleForTesting @@ -1164,13 +1180,17 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation if (DEBUG) Log.d(TAG, "user animation disabled types: " + disabledTypes); types &= ~mDisabledUserAnimationInsetsTypes; - if (fromIme && (disabledTypes & ime()) != 0 - && !mState.getSource(mImeSourceConsumer.getId()).isVisible()) { - // We've requested IMM to show IME, but the IME is not controllable. We need to - // cancel the request. - setRequestedVisibleTypes(0 /* visibleTypes */, ime()); - if (mImeSourceConsumer.onAnimationStateChanged(false /* running */)) { - notifyVisibilityChanged(); + if ((disabledTypes & ime()) != 0) { + ImeTracker.get().onFailed(statsToken, + ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION); + + if (fromIme && !mState.getSource(mImeSourceConsumer.getId()).isVisible()) { + // We've requested IMM to show IME, but the IME is not controllable. We need to + // cancel the request. + setRequestedVisibleTypes(0 /* visibleTypes */, ime()); + if (mImeSourceConsumer.onAnimationStateChanged(false /* running */)) { + notifyVisibilityChanged(); + } } } } @@ -1182,6 +1202,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation Trace.asyncTraceEnd(TRACE_TAG_VIEW, "IC.showRequestFromApiToImeReady", 0); return; } + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_DISABLED_USER_ANIMATION); + cancelExistingControllers(types); if (DEBUG) Log.d(TAG, "controlAnimation types: " + types); mLastStartedAnimTypes |= types; @@ -1189,7 +1211,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation final SparseArray controls = new SparseArray<>(); Pair typesReadyPair = collectSourceControls( - fromIme, types, controls, animationType); + fromIme, types, controls, animationType, statsToken); int typesReady = typesReadyPair.first; boolean imeReady = typesReadyPair.second; if (DEBUG) Log.d(TAG, String.format( @@ -1288,7 +1310,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation * @return Pair of (types ready to animate, IME ready to animate). */ private Pair collectSourceControls(boolean fromIme, @InsetsType int types, - SparseArray controls, @AnimationType int animationType) { + SparseArray controls, @AnimationType int animationType, + @Nullable ImeTracker.Token statsToken) { + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS); + int typesReady = 0; boolean imeReady = true; for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { @@ -1301,7 +1326,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation boolean canRun = true; if (show) { // Show request - switch(consumer.requestShow(fromIme)) { + switch(consumer.requestShow(fromIme, statsToken)) { case ShowResult.SHOW_IMMEDIATELY: break; case ShowResult.IME_SHOW_DELAYED: diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java index f46eb34bd3978..47672a3ec18e3 100644 --- a/core/java/android/view/InsetsSourceConsumer.java +++ b/core/java/android/view/InsetsSourceConsumer.java @@ -35,6 +35,7 @@ import android.util.Log; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets.Type.InsetsType; +import android.view.inputmethod.ImeTracker; import com.android.internal.annotations.VisibleForTesting; @@ -50,10 +51,14 @@ import java.util.function.Supplier; public class InsetsSourceConsumer { @Retention(RetentionPolicy.SOURCE) - @IntDef(value = {ShowResult.SHOW_IMMEDIATELY, ShowResult.IME_SHOW_DELAYED, ShowResult.IME_SHOW_FAILED}) + @IntDef(value = { + ShowResult.SHOW_IMMEDIATELY, + ShowResult.IME_SHOW_DELAYED, + ShowResult.IME_SHOW_FAILED + }) @interface ShowResult { /** - * Window type is ready to be shown, will be shown immidiately. + * Window type is ready to be shown, will be shown immediately. */ int SHOW_IMMEDIATELY = 0; /** @@ -71,11 +76,13 @@ public class InsetsSourceConsumer { protected final InsetsController mController; protected final InsetsState mState; private int mId; - private final @InsetsType int mType; + @InsetsType + private final int mType; private static final String TAG = "InsetsSourceConsumer"; private final Supplier mTransactionSupplier; - private @Nullable InsetsSourceControl mSourceControl; + @Nullable + private InsetsSourceControl mSourceControl; private boolean mHasWindowFocus; /** @@ -180,7 +187,7 @@ public class InsetsSourceConsumer { return true; } - @VisibleForTesting + @VisibleForTesting(visibility = PACKAGE) public InsetsSourceControl getControl() { return mSourceControl; } @@ -280,10 +287,16 @@ public class InsetsSourceConsumer { * @param fromController {@code true} if request is coming from controller. * (e.g. in IME case, controller is * {@link android.inputmethodservice.InputMethodService}). + * @param statsToken the token tracking the current IME show request or {@code null} otherwise. + * + * @implNote The {@code statsToken} is ignored here, and only handled in + * {@link ImeInsetsSourceConsumer} for IME animations only. + * * @return @see {@link ShowResult}. */ - @VisibleForTesting - public @ShowResult int requestShow(boolean fromController) { + @VisibleForTesting(visibility = PACKAGE) + @ShowResult + public int requestShow(boolean fromController, @Nullable ImeTracker.Token statsToken) { return ShowResult.SHOW_IMMEDIATELY; } diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java index 6eae63afcba7b..f08f61f99696d 100644 --- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java +++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java @@ -40,6 +40,7 @@ import com.android.internal.inputmethod.InputBindResult; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.inputmethod.StartInputFlags; import com.android.internal.inputmethod.StartInputReason; +import com.android.internal.view.IImeTracker; import com.android.internal.view.IInputMethodManager; import java.util.ArrayList; @@ -61,6 +62,9 @@ final class IInputMethodManagerGlobalInvoker { @Nullable private static volatile IInputMethodManager sServiceCache = null; + @Nullable + private static volatile IImeTracker sTrackerServiceCache = null; + /** * @return {@code true} if {@link IInputMethodManager} is available. */ @@ -527,4 +531,137 @@ final class IInputMethodManagerGlobalInvoker { throw e.rethrowFromSystemServer(); } } + + @AnyThread + @Nullable + static IBinder onRequestShow(int uid, @ImeTracker.Origin int origin, + @SoftInputShowHideReason int reason) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return null; + } + try { + return service.onRequestShow(uid, origin, reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @Nullable + static IBinder onRequestHide(int uid, @ImeTracker.Origin int origin, + @SoftInputShowHideReason int reason) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return null; + } + try { + return service.onRequestHide(uid, origin, reason); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void onProgress(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return; + } + try { + service.onProgress(statsToken, phase); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void onFailed(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return; + } + try { + service.onFailed(statsToken, phase); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void onCancelled(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return; + } + try { + service.onCancelled(statsToken, phase); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void onShown(@NonNull IBinder statsToken) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return; + } + try { + service.onShown(statsToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + static void onHidden(@NonNull IBinder statsToken) { + final IImeTracker service = getImeTrackerService(); + if (service == null) { + return; + } + try { + service.onHidden(statsToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) + static boolean hasPendingImeVisibilityRequests() { + final var service = getImeTrackerService(); + if (service == null) { + return true; + } + try { + return service.hasPendingImeVisibilityRequests(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @AnyThread + @Nullable + private static IImeTracker getImeTrackerService() { + var trackerService = sTrackerServiceCache; + if (trackerService == null) { + final var service = getService(); + if (service == null) { + return null; + } + + try { + trackerService = service.getImeTrackerService(); + if (trackerService == null) { + return null; + } + + sTrackerServiceCache = trackerService; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return trackerService; + } } diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index 927d7698985bf..1bcb04058de62 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -16,9 +16,6 @@ package android.view.inputmethod; -import static android.view.inputmethod.ImeTracker.Debug.originToString; -import static android.view.inputmethod.ImeTracker.Debug.phaseToString; - import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -46,6 +43,46 @@ public interface ImeTracker { String TAG = "ImeTracker"; + /** The type of the IME request. */ + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_SHOW, + TYPE_HIDE + }) + @Retention(RetentionPolicy.SOURCE) + @interface Type {} + + /** IME show request type. */ + int TYPE_SHOW = ImeProtoEnums.TYPE_SHOW; + + /** IME hide request type. */ + int TYPE_HIDE = ImeProtoEnums.TYPE_HIDE; + + /** The status of the IME request. */ + @IntDef(prefix = { "STATUS_" }, value = { + STATUS_RUN, + STATUS_CANCEL, + STATUS_FAIL, + STATUS_SUCCESS, + STATUS_TIMEOUT + }) + @Retention(RetentionPolicy.SOURCE) + @interface Status {} + + /** IME request running. */ + int STATUS_RUN = ImeProtoEnums.STATUS_RUN; + + /** IME request cancelled. */ + int STATUS_CANCEL = ImeProtoEnums.STATUS_CANCEL; + + /** IME request failed. */ + int STATUS_FAIL = ImeProtoEnums.STATUS_FAIL; + + /** IME request succeeded. */ + int STATUS_SUCCESS = ImeProtoEnums.STATUS_SUCCESS; + + /** IME request timed out. */ + int STATUS_TIMEOUT = ImeProtoEnums.STATUS_TIMEOUT; + /** * The origin of the IME request * @@ -61,25 +98,17 @@ public interface ImeTracker { @Retention(RetentionPolicy.SOURCE) @interface Origin {} - /** - * The IME show request originated in the client. - */ - int ORIGIN_CLIENT_SHOW_SOFT_INPUT = 0; + /** The IME show request originated in the client. */ + int ORIGIN_CLIENT_SHOW_SOFT_INPUT = ImeProtoEnums.ORIGIN_CLIENT_SHOW_SOFT_INPUT; - /** - * The IME hide request originated in the client. - */ - int ORIGIN_CLIENT_HIDE_SOFT_INPUT = 1; + /** The IME hide request originated in the client. */ + int ORIGIN_CLIENT_HIDE_SOFT_INPUT = ImeProtoEnums.ORIGIN_CLIENT_HIDE_SOFT_INPUT; - /** - * The IME show request originated in the server. - */ - int ORIGIN_SERVER_START_INPUT = 2; + /** The IME show request originated in the server. */ + int ORIGIN_SERVER_START_INPUT = ImeProtoEnums.ORIGIN_SERVER_START_INPUT; - /** - * The IME hide request originated in the server. - */ - int ORIGIN_SERVER_HIDE_INPUT = 3; + /** The IME hide request originated in the server. */ + int ORIGIN_SERVER_HIDE_INPUT = ImeProtoEnums.ORIGIN_SERVER_HIDE_INPUT; /** * The current phase of the IME request. @@ -88,6 +117,7 @@ public interface ImeTracker { * where the phase is (i.e. {@code PHASE_SERVER_...} occurs in the server). */ @IntDef(prefix = { "PHASE_" }, value = { + PHASE_NOT_SET, PHASE_CLIENT_VIEW_SERVED, PHASE_SERVER_CLIENT_KNOWN, PHASE_SERVER_CLIENT_FOCUSED, @@ -121,6 +151,11 @@ public interface ImeTracker { PHASE_CLIENT_HANDLE_HIDE_INSETS, PHASE_CLIENT_APPLY_ANIMATION, PHASE_CLIENT_CONTROL_ANIMATION, + PHASE_CLIENT_DISABLED_USER_ANIMATION, + PHASE_CLIENT_COLLECT_SOURCE_CONTROLS, + PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW, + PHASE_CLIENT_REQUEST_IME_SHOW, + PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN, PHASE_CLIENT_ANIMATION_RUNNING, PHASE_CLIENT_ANIMATION_CANCEL, PHASE_CLIENT_ANIMATION_FINISHED_SHOW, @@ -129,135 +164,172 @@ public interface ImeTracker { @Retention(RetentionPolicy.SOURCE) @interface Phase {} + int PHASE_NOT_SET = ImeProtoEnums.PHASE_NOT_SET; + /** The view that requested the IME has been served by the IMM. */ - int PHASE_CLIENT_VIEW_SERVED = 0; + int PHASE_CLIENT_VIEW_SERVED = ImeProtoEnums.PHASE_CLIENT_VIEW_SERVED; /** The IME client that requested the IME has window manager focus. */ - int PHASE_SERVER_CLIENT_KNOWN = 1; + int PHASE_SERVER_CLIENT_KNOWN = ImeProtoEnums.PHASE_SERVER_CLIENT_KNOWN; /** The IME client that requested the IME has IME focus. */ - int PHASE_SERVER_CLIENT_FOCUSED = 2; + int PHASE_SERVER_CLIENT_FOCUSED = ImeProtoEnums.PHASE_SERVER_CLIENT_FOCUSED; /** The IME request complies with the current accessibility settings. */ - int PHASE_SERVER_ACCESSIBILITY = 3; + int PHASE_SERVER_ACCESSIBILITY = ImeProtoEnums.PHASE_SERVER_ACCESSIBILITY; /** The server is ready to run third party code. */ - int PHASE_SERVER_SYSTEM_READY = 4; + int PHASE_SERVER_SYSTEM_READY = ImeProtoEnums.PHASE_SERVER_SYSTEM_READY; /** Checked the implicit hide request against any explicit show requests. */ - int PHASE_SERVER_HIDE_IMPLICIT = 5; + int PHASE_SERVER_HIDE_IMPLICIT = ImeProtoEnums.PHASE_SERVER_HIDE_IMPLICIT; /** Checked the not-always hide request against any forced show requests. */ - int PHASE_SERVER_HIDE_NOT_ALWAYS = 6; + int PHASE_SERVER_HIDE_NOT_ALWAYS = ImeProtoEnums.PHASE_SERVER_HIDE_NOT_ALWAYS; /** The server is waiting for a connection to the IME. */ - int PHASE_SERVER_WAIT_IME = 7; + int PHASE_SERVER_WAIT_IME = ImeProtoEnums.PHASE_SERVER_WAIT_IME; /** The server has a connection to the IME. */ - int PHASE_SERVER_HAS_IME = 8; + int PHASE_SERVER_HAS_IME = ImeProtoEnums.PHASE_SERVER_HAS_IME; /** The server decided the IME should be hidden. */ - int PHASE_SERVER_SHOULD_HIDE = 9; + int PHASE_SERVER_SHOULD_HIDE = ImeProtoEnums.PHASE_SERVER_SHOULD_HIDE; /** Reached the IME wrapper. */ - int PHASE_IME_WRAPPER = 10; + int PHASE_IME_WRAPPER = ImeProtoEnums.PHASE_IME_WRAPPER; /** Dispatched from the IME wrapper to the IME. */ - int PHASE_IME_WRAPPER_DISPATCH = 11; + int PHASE_IME_WRAPPER_DISPATCH = ImeProtoEnums.PHASE_IME_WRAPPER_DISPATCH; /** Reached the IME' showSoftInput method. */ - int PHASE_IME_SHOW_SOFT_INPUT = 12; + int PHASE_IME_SHOW_SOFT_INPUT = ImeProtoEnums.PHASE_IME_SHOW_SOFT_INPUT; /** Reached the IME' hideSoftInput method. */ - int PHASE_IME_HIDE_SOFT_INPUT = 13; + int PHASE_IME_HIDE_SOFT_INPUT = ImeProtoEnums.PHASE_IME_HIDE_SOFT_INPUT; /** The server decided the IME should be shown. */ - int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = 14; + int PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE = ImeProtoEnums.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE; /** Requested applying the IME visibility in the insets source consumer. */ - int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = 15; + int PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER = + ImeProtoEnums.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER; /** Applied the IME visibility. */ - int PHASE_SERVER_APPLY_IME_VISIBILITY = 16; + int PHASE_SERVER_APPLY_IME_VISIBILITY = ImeProtoEnums.PHASE_SERVER_APPLY_IME_VISIBILITY; /** Created the show IME runner. */ - int PHASE_WM_SHOW_IME_RUNNER = 17; + int PHASE_WM_SHOW_IME_RUNNER = ImeProtoEnums.PHASE_WM_SHOW_IME_RUNNER; /** Ready to show IME. */ - int PHASE_WM_SHOW_IME_READY = 18; + int PHASE_WM_SHOW_IME_READY = ImeProtoEnums.PHASE_WM_SHOW_IME_READY; /** The Window Manager has a connection to the IME insets control target. */ - int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = 19; + int PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET = + ImeProtoEnums.PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET; /** Reached the window insets control target's show insets method. */ - int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = 20; + int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS = + ImeProtoEnums.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS; /** Reached the window insets control target's hide insets method. */ - int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = 21; + int PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS = + ImeProtoEnums.PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS; /** Reached the remote insets control target's show insets method. */ - int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = 22; + int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS = + ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS; /** Reached the remote insets control target's hide insets method. */ - int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = 23; + int PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS = + ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS; /** Reached the remote insets controller. */ - int PHASE_WM_REMOTE_INSETS_CONTROLLER = 24; + int PHASE_WM_REMOTE_INSETS_CONTROLLER = ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROLLER; /** Created the IME window insets show animation. */ - int PHASE_WM_ANIMATION_CREATE = 25; + int PHASE_WM_ANIMATION_CREATE = ImeProtoEnums.PHASE_WM_ANIMATION_CREATE; /** Started the IME window insets show animation. */ - int PHASE_WM_ANIMATION_RUNNING = 26; + int PHASE_WM_ANIMATION_RUNNING = ImeProtoEnums.PHASE_WM_ANIMATION_RUNNING; /** Reached the client's show insets method. */ - int PHASE_CLIENT_SHOW_INSETS = 27; + int PHASE_CLIENT_SHOW_INSETS = ImeProtoEnums.PHASE_CLIENT_SHOW_INSETS; /** Reached the client's hide insets method. */ - int PHASE_CLIENT_HIDE_INSETS = 28; + int PHASE_CLIENT_HIDE_INSETS = ImeProtoEnums.PHASE_CLIENT_HIDE_INSETS; /** Handling the IME window insets show request. */ - int PHASE_CLIENT_HANDLE_SHOW_INSETS = 29; + int PHASE_CLIENT_HANDLE_SHOW_INSETS = ImeProtoEnums.PHASE_CLIENT_HANDLE_SHOW_INSETS; /** Handling the IME window insets hide request. */ - int PHASE_CLIENT_HANDLE_HIDE_INSETS = 30; + int PHASE_CLIENT_HANDLE_HIDE_INSETS = ImeProtoEnums.PHASE_CLIENT_HANDLE_HIDE_INSETS; /** Applied the IME window insets show animation. */ - int PHASE_CLIENT_APPLY_ANIMATION = 31; + int PHASE_CLIENT_APPLY_ANIMATION = ImeProtoEnums.PHASE_CLIENT_APPLY_ANIMATION; /** Started the IME window insets show animation. */ - int PHASE_CLIENT_CONTROL_ANIMATION = 32; + int PHASE_CLIENT_CONTROL_ANIMATION = ImeProtoEnums.PHASE_CLIENT_CONTROL_ANIMATION; + + /** Checked that the IME is controllable. */ + int PHASE_CLIENT_DISABLED_USER_ANIMATION = ImeProtoEnums.PHASE_CLIENT_DISABLED_USER_ANIMATION; + + /** Collecting insets source controls. */ + int PHASE_CLIENT_COLLECT_SOURCE_CONTROLS = ImeProtoEnums.PHASE_CLIENT_COLLECT_SOURCE_CONTROLS; + + /** Reached the insets source consumer's show request method. */ + int PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW = + ImeProtoEnums.PHASE_CLIENT_INSETS_CONSUMER_REQUEST_SHOW; + + /** Reached input method manager's request IME show method. */ + int PHASE_CLIENT_REQUEST_IME_SHOW = ImeProtoEnums.PHASE_CLIENT_REQUEST_IME_SHOW; + + /** Reached the insets source consumer's notify hidden method. */ + int PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN = + ImeProtoEnums.PHASE_CLIENT_INSETS_CONSUMER_NOTIFY_HIDDEN; /** Queued the IME window insets show animation. */ - int PHASE_CLIENT_ANIMATION_RUNNING = 33; + int PHASE_CLIENT_ANIMATION_RUNNING = ImeProtoEnums.PHASE_CLIENT_ANIMATION_RUNNING; /** Cancelled the IME window insets show animation. */ - int PHASE_CLIENT_ANIMATION_CANCEL = 34; + int PHASE_CLIENT_ANIMATION_CANCEL = ImeProtoEnums.PHASE_CLIENT_ANIMATION_CANCEL; /** Finished the IME window insets show animation. */ - int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = 35; + int PHASE_CLIENT_ANIMATION_FINISHED_SHOW = ImeProtoEnums.PHASE_CLIENT_ANIMATION_FINISHED_SHOW; /** Finished the IME window insets hide animation. */ - int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = 36; + int PHASE_CLIENT_ANIMATION_FINISHED_HIDE = ImeProtoEnums.PHASE_CLIENT_ANIMATION_FINISHED_HIDE; /** - * Called when an IME show request is created. + * Creates an IME show request tracking token. * - * @param token the token tracking the current IME show request or {@code null} otherwise. + * @param component the component name where the IME show request was created, + * or {@code null} otherwise + * (defaulting to {@link ActivityThread#currentProcessName()}). + * @param uid the uid of the client that requested the IME. * @param origin the origin of the IME show request. * @param reason the reason why the IME show request was created. + * + * @return An IME tracking token. */ - void onRequestShow(@Nullable Token token, @Origin int origin, + @NonNull + Token onRequestShow(@Nullable String component, int uid, @Origin int origin, @SoftInputShowHideReason int reason); /** - * Called when an IME hide request is created. + * Creates an IME hide request tracking token. * - * @param token the token tracking the current IME hide request or {@code null} otherwise. + * @param component the component name where the IME hide request was created, + * or {@code null} otherwise + * (defaulting to {@link ActivityThread#currentProcessName()}). + * @param uid the uid of the client that requested the IME. * @param origin the origin of the IME hide request. * @param reason the reason why the IME hide request was created. + * + * @return An IME tracking token. */ - void onRequestHide(@Nullable Token token, @Origin int origin, + @NonNull + Token onRequestHide(@Nullable String component, int uid, @Origin int origin, @SoftInputShowHideReason int reason); /** @@ -313,112 +385,122 @@ public interface ImeTracker { */ @NonNull static ImeTracker get() { - return SystemProperties.getBoolean("persist.debug.imetracker", false) - ? LOGGER - : NOOP_LOGGER; + return LOGGER; } /** The singleton IME tracker instance. */ + @NonNull ImeTracker LOGGER = new ImeTracker() { + { + // Set logging flag initial value. + mLogProgress = SystemProperties.getBoolean("persist.debug.imetracker", false); + // Update logging flag dynamically. + SystemProperties.addChangeCallback(() -> + mLogProgress = + SystemProperties.getBoolean("persist.debug.imetracker", false)); + } + + /** Whether progress should be logged. */ + private boolean mLogProgress; + + @NonNull @Override - public void onRequestShow(@Nullable Token token, int origin, + public Token onRequestShow(@Nullable String component, int uid, @Origin int origin, @SoftInputShowHideReason int reason) { - if (token == null) return; - Log.i(TAG, token.mTag + ": onRequestShow at " + originToString(origin) + IBinder binder = IInputMethodManagerGlobalInvoker.onRequestShow(uid, origin, reason); + if (binder == null) binder = new Binder(); + + final Token token = Token.build(binder, component); + + Log.i(TAG, token.mTag + ": onRequestShow at " + Debug.originToString(origin) + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason)); + + return token; } + @NonNull @Override - public void onRequestHide(@Nullable Token token, int origin, + public Token onRequestHide(@Nullable String component, int uid, @Origin int origin, @SoftInputShowHideReason int reason) { - if (token == null) return; - Log.i(TAG, token.mTag + ": onRequestHide at " + originToString(origin) + IBinder binder = IInputMethodManagerGlobalInvoker.onRequestHide(uid, origin, reason); + if (binder == null) binder = new Binder(); + + final Token token = Token.build(binder, component); + + Log.i(TAG, token.mTag + ": onRequestHide at " + Debug.originToString(origin) + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason)); + + return token; } @Override - public void onProgress(@Nullable Token token, int phase) { + public void onProgress(@Nullable Token token, @Phase int phase) { if (token == null) return; - Log.i(TAG, token.mTag + ": onProgress at " + phaseToString(phase)); + IInputMethodManagerGlobalInvoker.onProgress(token.mBinder, phase); + + if (mLogProgress) { + Log.i(TAG, token.mTag + ": onProgress at " + Debug.phaseToString(phase)); + } } @Override - public void onFailed(@Nullable Token token, int phase) { + public void onFailed(@Nullable Token token, @Phase int phase) { if (token == null) return; - Log.i(TAG, token.mTag + ": onFailed at " + phaseToString(phase)); + IInputMethodManagerGlobalInvoker.onFailed(token.mBinder, phase); + + Log.i(TAG, token.mTag + ": onFailed at " + Debug.phaseToString(phase)); } @Override - public void onTodo(@Nullable Token token, int phase) { + public void onTodo(@Nullable Token token, @Phase int phase) { if (token == null) return; - Log.i(TAG, token.mTag + ": onTodo at " + phaseToString(phase)); + Log.i(TAG, token.mTag + ": onTodo at " + Debug.phaseToString(phase)); } @Override - public void onCancelled(@Nullable Token token, int phase) { + public void onCancelled(@Nullable Token token, @Phase int phase) { if (token == null) return; - Log.i(TAG, token.mTag + ": onCancelled at " + phaseToString(phase)); + IInputMethodManagerGlobalInvoker.onCancelled(token.mBinder, phase); + + Log.i(TAG, token.mTag + ": onCancelled at " + Debug.phaseToString(phase)); } @Override public void onShown(@Nullable Token token) { if (token == null) return; + IInputMethodManagerGlobalInvoker.onShown(token.mBinder); + Log.i(TAG, token.mTag + ": onShown"); } @Override public void onHidden(@Nullable Token token) { if (token == null) return; + IInputMethodManagerGlobalInvoker.onHidden(token.mBinder); + Log.i(TAG, token.mTag + ": onHidden"); } }; - /** The singleton no-op IME tracker instance. */ - ImeTracker NOOP_LOGGER = new ImeTracker() { - - @Override - public void onRequestShow(@Nullable Token token, int origin, - @SoftInputShowHideReason int reason) {} - - @Override - public void onRequestHide(@Nullable Token token, int origin, - @SoftInputShowHideReason int reason) {} - - @Override - public void onProgress(@Nullable Token token, int phase) {} - - @Override - public void onFailed(@Nullable Token token, int phase) {} - - @Override - public void onTodo(@Nullable Token token, int phase) {} - - @Override - public void onCancelled(@Nullable Token token, int phase) {} - - @Override - public void onShown(@Nullable Token token) {} - - @Override - public void onHidden(@Nullable Token token) {} - }; - /** A token that tracks the progress of an IME request. */ class Token implements Parcelable { - private final IBinder mBinder; + @NonNull + public final IBinder mBinder; + + @NonNull private final String mTag; - public Token() { - this(ActivityThread.currentProcessName()); + @NonNull + private static Token build(@NonNull IBinder binder, @Nullable String component) { + if (component == null) component = ActivityThread.currentProcessName(); + final String tag = component + ":" + Integer.toHexString((new Random().nextInt())); + + return new Token(binder, tag); } - public Token(String component) { - this(new Binder(), component + ":" + Integer.toHexString((new Random().nextInt()))); - } - - private Token(IBinder binder, String tag) { + private Token(@NonNull IBinder binder, @NonNull String tag) { mBinder = binder; mTag = tag; } @@ -443,10 +525,11 @@ public interface ImeTracker { @NonNull public static final Creator CREATOR = new Creator<>() { + @NonNull @Override public Token createFromParcel(Parcel source) { - IBinder binder = source.readStrongBinder(); - String tag = source.readString8(); + final IBinder binder = source.readStrongBinder(); + final String tag = source.readString8(); return new Token(binder, tag); } @@ -458,22 +541,34 @@ public interface ImeTracker { } /** - * Utilities for mapping phases and origins IntDef values to their names. + * Utilities for mapping IntDef values to their names. * * Note: This is held in a separate class so that it only gets initialized when actually needed. */ class Debug { + private static final Map sTypes = + getFieldMapping(ImeTracker.class, "TYPE_"); + private static final Map sStatus = + getFieldMapping(ImeTracker.class, "STATUS_"); private static final Map sOrigins = getFieldMapping(ImeTracker.class, "ORIGIN_"); private static final Map sPhases = getFieldMapping(ImeTracker.class, "PHASE_"); - public static String originToString(int origin) { + public static String typeToString(@Type int type) { + return sTypes.getOrDefault(type, "TYPE_" + type); + } + + public static String statusToString(@Status int status) { + return sStatus.getOrDefault(status, "STATUS_" + status); + } + + public static String originToString(@Origin int origin) { return sOrigins.getOrDefault(origin, "ORIGIN_" + origin); } - public static String phaseToString(int phase) { + public static String phaseToString(@Phase int phase) { return sPhases.getOrDefault(phase, "PHASE_" + phase); } diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 83311559ed354..e9f0d2961eba9 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -2002,14 +2002,16 @@ public final class InputMethodManager { * {@link #RESULT_HIDDEN}. */ public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) { - return showSoftInput(view, flags, resultReceiver, SoftInputShowHideReason.SHOW_SOFT_INPUT); + return showSoftInput(view, null /* statsToken */, flags, resultReceiver, + SoftInputShowHideReason.SHOW_SOFT_INPUT); } - private boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver, - @SoftInputShowHideReason int reason) { - final ImeTracker.Token statsToken = new ImeTracker.Token(); - ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, - reason); + private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken, int flags, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { + if (statsToken == null) { + statsToken = ImeTracker.get().onRequestShow(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason); + } ImeTracing.getInstance().triggerClientDump("InputMethodManager#showSoftInput", this, null /* icProto */); @@ -2057,8 +2059,8 @@ public final class InputMethodManager { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499) public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) { synchronized (mH) { - final ImeTracker.Token statsToken = new ImeTracker.Token(); - ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, + final ImeTracker.Token statsToken = ImeTracker.get().onRequestShow(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, SoftInputShowHideReason.SHOW_SOFT_INPUT); Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be" @@ -2148,9 +2150,8 @@ public final class InputMethodManager { private boolean hideSoftInputFromWindow(IBinder windowToken, int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { - final ImeTracker.Token statsToken = new ImeTracker.Token(); - ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, - reason); + final ImeTracker.Token statsToken = ImeTracker.get().onRequestHide(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, reason); ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow", this, null /* icProto */); @@ -2283,7 +2284,7 @@ public final class InputMethodManager { hideSoftInputFromWindow(view.getWindowToken(), hideFlags, null, SoftInputShowHideReason.HIDE_TOGGLE_SOFT_INPUT); } else { - showSoftInput(view, showFlags, null, + showSoftInput(view, null /* statsToken */, showFlags, null /* resultReceiver */, SoftInputShowHideReason.SHOW_TOGGLE_SOFT_INPUT); } } @@ -2796,8 +2797,8 @@ public final class InputMethodManager { @UnsupportedAppUsage void closeCurrentInput() { - final ImeTracker.Token statsToken = new ImeTracker.Token(); - ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, + final ImeTracker.Token statsToken = ImeTracker.get().onRequestHide(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, SoftInputShowHideReason.HIDE_SOFT_INPUT); synchronized (mH) { @@ -2856,18 +2857,23 @@ public final class InputMethodManager { * * @param windowToken the window from which this request originates. If this doesn't match the * currently served view, the request is ignored and returns {@code false}. + * @param statsToken the token tracking the current IME show request or {@code null} otherwise. * * @return {@code true} if IME can (eventually) be shown, {@code false} otherwise. * @hide */ - public boolean requestImeShow(IBinder windowToken) { + public boolean requestImeShow(IBinder windowToken, @Nullable ImeTracker.Token statsToken) { checkFocus(); synchronized (mH) { final View servedView = getServedViewLocked(); if (servedView == null || servedView.getWindowToken() != windowToken) { + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_CLIENT_REQUEST_IME_SHOW); return false; } - showSoftInput(servedView, 0 /* flags */, null /* resultReceiver */, + + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_CLIENT_REQUEST_IME_SHOW); + + showSoftInput(servedView, statsToken, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API); return true; } @@ -2878,12 +2884,15 @@ public final class InputMethodManager { * * @param windowToken the window from which this request originates. If this doesn't match the * currently served view, the request is ignored. + * @param statsToken the token tracking the current IME show request or {@code null} otherwise. * @hide */ - public void notifyImeHidden(IBinder windowToken) { - final ImeTracker.Token statsToken = new ImeTracker.Token(); - ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, - SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); + public void notifyImeHidden(IBinder windowToken, @Nullable ImeTracker.Token statsToken) { + if (statsToken == null) { + statsToken = ImeTracker.get().onRequestHide(null /* component */, + Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, + SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); + } ImeTracing.getInstance().triggerClientDump("InputMethodManager#notifyImeHidden", this, null /* icProto */); @@ -3548,6 +3557,18 @@ public final class InputMethodManager { return IInputMethodManagerGlobalInvoker.isInputMethodPickerShownForTest(); } + /** + * A test API for CTS to check whether there are any pending IME visibility requests. + * + * @return {@code true} iff there are pending IME visibility requests. + * @hide + */ + @TestApi + @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD) + public boolean hasPendingImeVisibilityRequests() { + return IInputMethodManagerGlobalInvoker.hasPendingImeVisibilityRequests(); + } + /** * Show the settings for enabling subtypes of the specified input method. * diff --git a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java index f1635ebe5fb3e..ec9184b72ed17 100644 --- a/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java +++ b/core/java/com/android/internal/inputmethod/SoftInputShowHideReason.java @@ -23,6 +23,7 @@ import android.os.IBinder; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ImeProtoEnums; import android.view.inputmethod.InputMethodManager; import java.lang.annotation.Retention; @@ -64,113 +65,114 @@ import java.lang.annotation.Retention; SoftInputShowHideReason.HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT, SoftInputShowHideReason.HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED, SoftInputShowHideReason.HIDE_SOFT_INPUT_IMM_DEPRECATION, - SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR}) + SoftInputShowHideReason.HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR +}) public @interface SoftInputShowHideReason { /** Show soft input by {@link android.view.inputmethod.InputMethodManager#showSoftInput}. */ - int SHOW_SOFT_INPUT = 0; + int SHOW_SOFT_INPUT = ImeProtoEnums.REASON_SHOW_SOFT_INPUT; /** Show soft input when {@code InputMethodManagerService#attachNewInputLocked} called. */ - int ATTACH_NEW_INPUT = 1; + int ATTACH_NEW_INPUT = ImeProtoEnums.REASON_ATTACH_NEW_INPUT; /** Show soft input by {@code InputMethodManagerService#showMySoftInput}. This is triggered when * the IME process try to show the keyboard. * * @see android.inputmethodservice.InputMethodService#requestShowSelf(int) */ - int SHOW_SOFT_INPUT_FROM_IME = 2; + int SHOW_SOFT_INPUT_FROM_IME = ImeProtoEnums.REASON_SHOW_SOFT_INPUT_FROM_IME; /** * Hide soft input by * {@link android.view.inputmethod.InputMethodManager#hideSoftInputFromWindow}. */ - int HIDE_SOFT_INPUT = 3; + int HIDE_SOFT_INPUT = ImeProtoEnums.REASON_HIDE_SOFT_INPUT; /** * Hide soft input by * {@link android.inputmethodservice.InputMethodService#requestHideSelf(int)}. */ - int HIDE_SOFT_INPUT_FROM_IME = 4; + int HIDE_SOFT_INPUT_FROM_IME = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_FROM_IME; /** * Show soft input when navigated forward to the window (with - * {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION}} which the focused view is text + * {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION}) which the focused view is text * editor and system will auto-show the IME when the window can resize or running on a large * screen. */ - int SHOW_AUTO_EDITOR_FORWARD_NAV = 5; + int SHOW_AUTO_EDITOR_FORWARD_NAV = ImeProtoEnums.REASON_SHOW_AUTO_EDITOR_FORWARD_NAV; /** * Show soft input when navigated forward to the window with * {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION} and * {@link LayoutParams#SOFT_INPUT_STATE_VISIBLE}. */ - int SHOW_STATE_VISIBLE_FORWARD_NAV = 6; + int SHOW_STATE_VISIBLE_FORWARD_NAV = ImeProtoEnums.REASON_SHOW_STATE_VISIBLE_FORWARD_NAV; /** * Show soft input when the window with {@link LayoutParams#SOFT_INPUT_STATE_ALWAYS_VISIBLE}. */ - int SHOW_STATE_ALWAYS_VISIBLE = 7; + int SHOW_STATE_ALWAYS_VISIBLE = ImeProtoEnums.REASON_SHOW_STATE_ALWAYS_VISIBLE; /** * Show soft input during {@code InputMethodManagerService} receive changes from * {@code SettingsProvider}. */ - int SHOW_SETTINGS_ON_CHANGE = 8; + int SHOW_SETTINGS_ON_CHANGE = ImeProtoEnums.REASON_SHOW_SETTINGS_ON_CHANGE; /** Hide soft input during switching user. */ - int HIDE_SWITCH_USER = 9; + int HIDE_SWITCH_USER = ImeProtoEnums.REASON_HIDE_SWITCH_USER; /** Hide soft input when the user is invalid. */ - int HIDE_INVALID_USER = 10; + int HIDE_INVALID_USER = ImeProtoEnums.REASON_HIDE_INVALID_USER; /** * Hide soft input when the window with {@link LayoutParams#SOFT_INPUT_STATE_UNSPECIFIED} which * the focused view is not text editor. */ - int HIDE_UNSPECIFIED_WINDOW = 11; + int HIDE_UNSPECIFIED_WINDOW = ImeProtoEnums.REASON_HIDE_UNSPECIFIED_WINDOW; /** * Hide soft input when navigated forward to the window with * {@link LayoutParams#SOFT_INPUT_IS_FORWARD_NAVIGATION} and * {@link LayoutParams#SOFT_INPUT_STATE_HIDDEN}. */ - int HIDE_STATE_HIDDEN_FORWARD_NAV = 12; + int HIDE_STATE_HIDDEN_FORWARD_NAV = ImeProtoEnums.REASON_HIDE_STATE_HIDDEN_FORWARD_NAV; /** * Hide soft input when the window with {@link LayoutParams#SOFT_INPUT_STATE_ALWAYS_HIDDEN}. */ - int HIDE_ALWAYS_HIDDEN_STATE = 13; + int HIDE_ALWAYS_HIDDEN_STATE = ImeProtoEnums.REASON_HIDE_ALWAYS_HIDDEN_STATE; /** Hide soft input when "adb shell ime " called. */ - int HIDE_RESET_SHELL_COMMAND = 14; + int HIDE_RESET_SHELL_COMMAND = ImeProtoEnums.REASON_HIDE_RESET_SHELL_COMMAND; /** * Hide soft input during {@code InputMethodManagerService} receive changes from * {@code SettingsProvider}. */ - int HIDE_SETTINGS_ON_CHANGE = 15; + int HIDE_SETTINGS_ON_CHANGE = ImeProtoEnums.REASON_HIDE_SETTINGS_ON_CHANGE; /** * Hide soft input from {@link com.android.server.policy.PhoneWindowManager} when setting * {@link com.android.internal.R.integer#config_shortPressOnPowerBehavior} in config.xml as * dismiss IME. */ - int HIDE_POWER_BUTTON_GO_HOME = 16; + int HIDE_POWER_BUTTON_GO_HOME = ImeProtoEnums.REASON_HIDE_POWER_BUTTON_GO_HOME; /** Hide soft input when attaching docked stack. */ - int HIDE_DOCKED_STACK_ATTACHED = 17; + int HIDE_DOCKED_STACK_ATTACHED = ImeProtoEnums.REASON_HIDE_DOCKED_STACK_ATTACHED; /** * Hide soft input when {@link com.android.server.wm.RecentsAnimationController} starts * intercept touch from app window. */ - int HIDE_RECENTS_ANIMATION = 18; + int HIDE_RECENTS_ANIMATION = ImeProtoEnums.REASON_HIDE_RECENTS_ANIMATION; /** * Hide soft input when {@link com.android.wm.shell.bubbles.BubbleController} is expanding, * switching, or collapsing Bubbles. */ - int HIDE_BUBBLES = 19; + int HIDE_BUBBLES = ImeProtoEnums.REASON_HIDE_BUBBLES; /** * Hide soft input when focusing the same window (e.g. screen turned-off and turn-on) which no @@ -183,74 +185,78 @@ public @interface SoftInputShowHideReason { * only the dialog focused as it's the latest window with input focus) makes we need to hide * soft-input when the same window focused again to align with the same behavior prior to R. */ - int HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR = 20; + int HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR = + ImeProtoEnums.REASON_HIDE_SAME_WINDOW_FOCUSED_WITHOUT_EDITOR; /** * Hide soft input when a {@link com.android.internal.inputmethod.IInputMethodClient} is * removed. */ - int HIDE_REMOVE_CLIENT = 21; + int HIDE_REMOVE_CLIENT = ImeProtoEnums.REASON_HIDE_REMOVE_CLIENT; /** * Show soft input when the system invoking * {@link com.android.server.wm.WindowManagerInternal#shouldRestoreImeVisibility}. */ - int SHOW_RESTORE_IME_VISIBILITY = 22; + int SHOW_RESTORE_IME_VISIBILITY = ImeProtoEnums.REASON_SHOW_RESTORE_IME_VISIBILITY; /** * Show soft input by * {@link android.view.inputmethod.InputMethodManager#toggleSoftInput(int, int)}; */ - int SHOW_TOGGLE_SOFT_INPUT = 23; + int SHOW_TOGGLE_SOFT_INPUT = ImeProtoEnums.REASON_SHOW_TOGGLE_SOFT_INPUT; /** * Hide soft input by * {@link android.view.inputmethod.InputMethodManager#toggleSoftInput(int, int)}; */ - int HIDE_TOGGLE_SOFT_INPUT = 24; + int HIDE_TOGGLE_SOFT_INPUT = ImeProtoEnums.REASON_HIDE_TOGGLE_SOFT_INPUT; /** * Show soft input by * {@link android.view.InsetsController#show(int)}; */ - int SHOW_SOFT_INPUT_BY_INSETS_API = 25; + int SHOW_SOFT_INPUT_BY_INSETS_API = ImeProtoEnums.REASON_SHOW_SOFT_INPUT_BY_INSETS_API; /** * Hide soft input if Ime policy has been set to {@link WindowManager#DISPLAY_IME_POLICY_HIDE}. * See also {@code InputMethodManagerService#mImeHiddenByDisplayPolicy}. */ - int HIDE_DISPLAY_IME_POLICY_HIDE = 26; + int HIDE_DISPLAY_IME_POLICY_HIDE = ImeProtoEnums.REASON_HIDE_DISPLAY_IME_POLICY_HIDE; /** * Hide soft input by {@link android.view.InsetsController#hide(int)}. */ - int HIDE_SOFT_INPUT_BY_INSETS_API = 27; + int HIDE_SOFT_INPUT_BY_INSETS_API = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_BY_INSETS_API; /** * Hide soft input by {@link android.inputmethodservice.InputMethodService#handleBack(boolean)}. */ - int HIDE_SOFT_INPUT_BY_BACK_KEY = 28; + int HIDE_SOFT_INPUT_BY_BACK_KEY = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_BY_BACK_KEY; /** * Hide soft input by * {@link android.inputmethodservice.InputMethodService#onToggleSoftInput(int, int)}. */ - int HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT = 29; + int HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT = + ImeProtoEnums.REASON_HIDE_SOFT_INPUT_IME_TOGGLE_SOFT_INPUT; /** * Hide soft input by * {@link android.inputmethodservice.InputMethodService#onExtractingInputChanged(EditorInfo)})}. */ - int HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED = 30; + int HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED = + ImeProtoEnums.REASON_HIDE_SOFT_INPUT_EXTRACT_INPUT_CHANGED; /** * Hide soft input by the deprecated * {@link InputMethodManager#hideSoftInputFromInputMethod(IBinder, int)}. */ - int HIDE_SOFT_INPUT_IMM_DEPRECATION = 31; + int HIDE_SOFT_INPUT_IMM_DEPRECATION = ImeProtoEnums.REASON_HIDE_SOFT_INPUT_IMM_DEPRECATION; /** * Hide soft input when the window gained focus without an editor from the IME shown window. */ - int HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR = 32; + int HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR = + ImeProtoEnums.REASON_HIDE_WINDOW_GAINED_FOCUS_WITHOUT_EDITOR; } diff --git a/core/java/com/android/internal/view/IImeTracker.aidl b/core/java/com/android/internal/view/IImeTracker.aidl new file mode 100644 index 0000000000000..b062ca7df9212 --- /dev/null +++ b/core/java/com/android/internal/view/IImeTracker.aidl @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 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 com.android.internal.view; + +import android.view.inputmethod.ImeTracker; + +/** + * Interface to the global Ime tracker, used by all client applications. + * {@hide} + */ +interface IImeTracker { + + /** + * Called when an IME show request is created, + * returns a new Binder to be associated with the IME tracking token. + * + * @param uid the uid of the client that requested the IME. + * @param origin the origin of the IME show request. + * @param reason the reason why the IME show request was created. + */ + IBinder onRequestShow(int uid, int origin, int reason); + + /** + * Called when an IME hide request is created, + * returns a new Binder to be associated with the IME tracking token. + * + * @param uid the uid of the client that requested the IME. + * @param origin the origin of the IME hide request. + * @param reason the reason why the IME hide request was created. + */ + IBinder onRequestHide(int uid, int origin, int reason); + + /** + * Called when the IME request progresses to a further phase. + * + * @param statsToken the token tracking the current IME request. + * @param phase the new phase the IME request reached. + */ + oneway void onProgress(in IBinder statsToken, int phase); + + /** + * Called when the IME request fails. + * + * @param statsToken the token tracking the current IME request. + * @param phase the phase the IME request failed at. + */ + oneway void onFailed(in IBinder statsToken, int phase); + + /** + * Called when the IME request is cancelled. + * + * @param statsToken the token tracking the current IME request. + * @param phase the phase the IME request was cancelled at. + */ + oneway void onCancelled(in IBinder statsToken, int phase); + + /** + * Called when the IME show request is successful. + * + * @param statsToken the token tracking the current IME request. + */ + oneway void onShown(in IBinder statsToken); + + /** + * Called when the IME hide request is successful. + * + * @param statsToken the token tracking the current IME request. + */ + oneway void onHidden(in IBinder statsToken); + + /** + * Checks whether there are any pending IME visibility requests. + * + * @return {@code true} iff there are pending IME visibility requests. + */ + @EnforcePermission("TEST_INPUT_METHOD") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.TEST_INPUT_METHOD)") + boolean hasPendingImeVisibilityRequests(); +} diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 00bc3f2ae5308..2106426c6c9b4 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -27,6 +27,7 @@ import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputBindResult; +import com.android.internal.view.IImeTracker; /** * Public interface to the global input method manager, used by all client @@ -158,4 +159,10 @@ interface IInputMethodManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.TEST_INPUT_METHOD)") void setStylusWindowIdleTimeoutForTest(in IInputMethodClient client, long timeout); + + /** + * Returns the singleton instance for the Ime Tracker Service. + * {@hide} + */ + IImeTracker getImeTrackerService(); } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index c917302443aef..5ec93e5937390 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -71,6 +71,7 @@ import android.view.WindowInsetsController.OnControllableInsetsChangedListener; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; import android.view.animation.LinearInterpolator; +import android.view.inputmethod.ImeTracker; import android.widget.TextView; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -136,7 +137,8 @@ public class InsetsControllerTest { private boolean mImeRequestedShow; @Override - public int requestShow(boolean fromController) { + public int requestShow(boolean fromController, + ImeTracker.Token statsToken) { if (fromController || mImeRequestedShow) { mImeRequestedShow = true; return SHOW_IMMEDIATELY; diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java index 3a3eeee77d8d0..0486e3c73f971 100644 --- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java @@ -43,6 +43,7 @@ import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl.Transaction; import android.view.WindowManager.BadTokenException; import android.view.WindowManager.LayoutParams; +import android.view.inputmethod.ImeTracker; import android.widget.TextView; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -221,7 +222,8 @@ public class InsetsSourceConsumerTest { return new InsetsSourceConsumer(ITYPE_IME, ime(), state, () -> mMockTransaction, controller) { @Override - public int requestShow(boolean fromController) { + public int requestShow(boolean fromController, + ImeTracker.Token statsToken) { return SHOW_IMMEDIATELY; } }; diff --git a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java new file mode 100644 index 0000000000000..60167b4e75d58 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2022 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 com.android.server.inputmethod; + +import android.Manifest; +import android.annotation.EnforcePermission; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.view.inputmethod.ImeTracker; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.view.IImeTracker; + +import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayDeque; +import java.util.Locale; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Service for managing and logging {@link ImeTracker.Token} instances. + * + * @implNote Suppresses {@link GuardedBy} warnings, as linter reports that {@link #mHistory} + * interactions are guarded by {@code this} instead of {@code ImeTrackerService.this}, which should + * be identical. + * + * @hide + */ +@SuppressWarnings("GuardedBy") +public final class ImeTrackerService extends IImeTracker.Stub { + + static final String TAG = "ImeTrackerService"; + + /** The threshold in milliseconds after which a history entry is considered timed out. */ + private static final long TIMEOUT_MS = 10_000; + + /** Handler for registering timeouts for live entries. */ + private final Handler mHandler = + new Handler(Looper.myLooper(), null /* callback */, true /* async */); + + /** Singleton instance of the History. */ + @GuardedBy("ImeTrackerService.this") + private final History mHistory = new History(); + + @NonNull + @Override + public synchronized IBinder onRequestShow(int uid, @ImeTracker.Origin int origin, + @SoftInputShowHideReason int reason) { + final IBinder binder = new Binder(); + final History.Entry entry = new History.Entry(uid, ImeTracker.TYPE_SHOW, + ImeTracker.STATUS_RUN, origin, reason); + mHistory.addEntry(binder, entry); + + // Register a delayed task to handle the case where the new entry times out. + mHandler.postDelayed(() -> { + synchronized (ImeTrackerService.this) { + mHistory.setFinished(binder, ImeTracker.STATUS_TIMEOUT, ImeTracker.PHASE_NOT_SET); + } + }, TIMEOUT_MS); + + return binder; + } + + @NonNull + @Override + public synchronized IBinder onRequestHide(int uid, @ImeTracker.Origin int origin, + @SoftInputShowHideReason int reason) { + final IBinder binder = new Binder(); + final History.Entry entry = new History.Entry(uid, ImeTracker.TYPE_HIDE, + ImeTracker.STATUS_RUN, origin, reason); + mHistory.addEntry(binder, entry); + + // Register a delayed task to handle the case where the new entry times out. + mHandler.postDelayed(() -> { + synchronized (ImeTrackerService.this) { + mHistory.setFinished(binder, ImeTracker.STATUS_TIMEOUT, ImeTracker.PHASE_NOT_SET); + } + }, TIMEOUT_MS); + + return binder; + } + + @Override + public synchronized void onProgress(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) { + final History.Entry entry = mHistory.getEntry(statsToken); + if (entry == null) return; + + entry.mPhase = phase; + } + + @Override + public synchronized void onFailed(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) { + mHistory.setFinished(statsToken, ImeTracker.STATUS_FAIL, phase); + } + + @Override + public synchronized void onCancelled(@NonNull IBinder statsToken, @ImeTracker.Phase int phase) { + mHistory.setFinished(statsToken, ImeTracker.STATUS_CANCEL, phase); + } + + @Override + public synchronized void onShown(@NonNull IBinder statsToken) { + mHistory.setFinished(statsToken, ImeTracker.STATUS_SUCCESS, ImeTracker.PHASE_NOT_SET); + } + + @Override + public synchronized void onHidden(@NonNull IBinder statsToken) { + mHistory.setFinished(statsToken, ImeTracker.STATUS_SUCCESS, ImeTracker.PHASE_NOT_SET); + } + + /** + * Updates the IME request tracking token with new information available in IMMS. + * + * @param statsToken the token corresponding to the current IME request. + * @param requestWindowName the name of the window that created the IME request. + */ + public synchronized void onImmsUpdate(@NonNull IBinder statsToken, + @NonNull String requestWindowName) { + final History.Entry entry = mHistory.getEntry(statsToken); + if (entry == null) return; + + entry.mRequestWindowName = requestWindowName; + } + + /** Dumps the contents of the history. */ + public synchronized void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + mHistory.dump(pw, prefix); + } + + @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @Override + public synchronized boolean hasPendingImeVisibilityRequests() { + super.hasPendingImeVisibilityRequests_enforcePermission(); + + return !mHistory.mLiveEntries.isEmpty(); + } + + /** + * A circular buffer storing the most recent few {@link ImeTracker.Token} entries information. + */ + private static final class History { + + /** The circular buffer's capacity. */ + private static final int CAPACITY = 100; + + /** Backing store for the circular buffer. */ + @GuardedBy("ImeTrackerService.this") + private final ArrayDeque mEntries = new ArrayDeque<>(CAPACITY); + + /** Backing store for the live entries (i.e. entries that are not finished yet). */ + @GuardedBy("ImeTrackerService.this") + private final WeakHashMap mLiveEntries = new WeakHashMap<>(); + + /** Latest entry sequence number. */ + private static final AtomicInteger sSequenceNumber = new AtomicInteger(0); + + /** Adds a live entry. */ + @GuardedBy("ImeTrackerService.this") + private void addEntry(@NonNull IBinder statsToken, @NonNull Entry entry) { + mLiveEntries.put(statsToken, entry); + } + + /** Gets the entry corresponding to the given IME tracking token, if it exists. */ + @Nullable + @GuardedBy("ImeTrackerService.this") + private Entry getEntry(@NonNull IBinder statsToken) { + return mLiveEntries.get(statsToken); + } + + /** + * Sets the live entry corresponding to the tracking token, if it exists, as finished, + * and uploads the data for metrics. + * + * @param statsToken the token corresponding to the current IME request. + * @param status the finish status of the IME request. + * @param phase the phase the IME request finished at, if it exists + * (or {@link ImeTracker#PHASE_NOT_SET} otherwise). + */ + @GuardedBy("ImeTrackerService.this") + private void setFinished(@NonNull IBinder statsToken, @ImeTracker.Status int status, + @ImeTracker.Phase int phase) { + final Entry entry = mLiveEntries.remove(statsToken); + if (entry == null) return; + + entry.mDuration = System.currentTimeMillis() - entry.mStartTime; + entry.mStatus = status; + + if (phase != ImeTracker.PHASE_NOT_SET) { + entry.mPhase = phase; + } + + // Remove excess entries overflowing capacity (plus one for the new entry). + while (mEntries.size() >= CAPACITY) { + mEntries.remove(); + } + + mEntries.offer(entry); + + // Log newly finished entry. + FrameworkStatsLog.write(FrameworkStatsLog.IME_REQUEST_FINISHED, entry.mUid, + entry.mDuration, entry.mType, entry.mStatus, entry.mReason, + entry.mOrigin, entry.mPhase); + } + + /** Dumps the contents of the circular buffer. */ + @GuardedBy("ImeTrackerService.this") + private void dump(@NonNull PrintWriter pw, @NonNull String prefix) { + final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + .withZone(ZoneId.systemDefault()); + + pw.print(prefix); + pw.println("ImeTrackerService#History.mLiveEntries:"); + + for (final Entry entry: mLiveEntries.values()) { + dumpEntry(entry, pw, prefix, formatter); + } + + pw.print(prefix); + pw.println("ImeTrackerService#History.mEntries:"); + + for (final Entry entry: mEntries) { + dumpEntry(entry, pw, prefix, formatter); + } + } + + @GuardedBy("ImeTrackerService.this") + private void dumpEntry(@NonNull Entry entry, @NonNull PrintWriter pw, + @NonNull String prefix, @NonNull DateTimeFormatter formatter) { + pw.print(prefix); + pw.println("ImeTrackerService#History #" + entry.mSequenceNumber + ":"); + + pw.print(prefix); + pw.println(" startTime=" + formatter.format(Instant.ofEpochMilli(entry.mStartTime))); + + pw.print(prefix); + pw.println(" duration=" + entry.mDuration + "ms"); + + pw.print(prefix); + pw.print(" type=" + ImeTracker.Debug.typeToString(entry.mType)); + + pw.print(prefix); + pw.print(" status=" + ImeTracker.Debug.statusToString(entry.mStatus)); + + pw.print(prefix); + pw.print(" origin=" + + ImeTracker.Debug.originToString(entry.mOrigin)); + + pw.print(prefix); + pw.print(" reason=" + + InputMethodDebug.softInputDisplayReasonToString(entry.mReason)); + + pw.print(prefix); + pw.print(" phase=" + + ImeTracker.Debug.phaseToString(entry.mPhase)); + + pw.print(prefix); + pw.print(" requestWindowName=" + entry.mRequestWindowName); + } + + /** A history entry. */ + private static final class Entry { + + /** The entry's sequence number in the history. */ + private final int mSequenceNumber = sSequenceNumber.getAndIncrement(); + + /** Uid of the client that requested the IME. */ + private final int mUid; + + /** Clock time in milliseconds when the IME request was created. */ + private final long mStartTime = System.currentTimeMillis(); + + /** Duration in milliseconds of the IME request from start to end. */ + private long mDuration = 0; + + /** Type of the IME request. */ + @ImeTracker.Type + private final int mType; + + /** Status of the IME request. */ + @ImeTracker.Status + private int mStatus; + + /** Origin of the IME request. */ + @ImeTracker.Origin + private final int mOrigin; + + /** Reason for creating the IME request. */ + @SoftInputShowHideReason + private final int mReason; + + /** Latest phase of the IME request. */ + @ImeTracker.Phase + private int mPhase = ImeTracker.PHASE_NOT_SET; + + /** + * Name of the window that created the IME request. + * + * Note: This is later set through {@link #onImmsUpdate(IBinder, String)}. + */ + @NonNull + private String mRequestWindowName = "not set"; + + private Entry(int uid, @ImeTracker.Type int type, @ImeTracker.Status int status, + @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) { + mUid = uid; + mType = type; + mStatus = status; + mOrigin = origin; + mReason = reason; + } + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 5840acfad6757..c15b538ce6054 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -176,6 +176,7 @@ import com.android.internal.os.TransferPipe; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.DumpUtils; +import com.android.internal.view.IImeTracker; import com.android.internal.view.IInputMethodManager; import com.android.server.AccessibilityManagerInternal; import com.android.server.EventLogTags; @@ -198,11 +199,12 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.security.InvalidParameterException; -import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -919,8 +921,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } void dump(@NonNull PrintWriter pw, @NonNull String prefix) { - final SimpleDateFormat dataFormat = - new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US); + final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + .withZone(ZoneId.systemDefault()); for (int i = 0; i < mEntries.length; ++i) { final Entry entry = mEntries[(i + mNextIndex) % mEntries.length]; @@ -931,7 +934,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub pw.println("SoftInputShowHideHistory #" + entry.mSequenceNumber + ":"); pw.print(prefix); - pw.println(" time=" + dataFormat.format(new Date(entry.mWallTime)) + pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime)) + " (timestamp=" + entry.mTimestamp + ")"); pw.print(prefix); @@ -999,7 +1002,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private static final int ENTRY_SIZE_FOR_HIGH_RAM_DEVICE = 32; /** - * Entry size for non low-RAM devices. + * Entry size for low-RAM devices. * *

TODO: Consider to follow what other system services have been doing to manage * constants (e.g. {@link android.provider.Settings.Global#ACTIVITY_MANAGER_CONSTANTS}).

@@ -1015,7 +1018,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } /** - * Backing store for the ring bugger. + * Backing store for the ring buffer. */ private final Entry[] mEntries = new Entry[getEntrySize()]; @@ -1095,8 +1098,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } void dump(@NonNull PrintWriter pw, @NonNull String prefix) { - final SimpleDateFormat dataFormat = - new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US); + final DateTimeFormatter formatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + .withZone(ZoneId.systemDefault()); for (int i = 0; i < mEntries.length; ++i) { final Entry entry = mEntries[(i + mNextIndex) % mEntries.length]; @@ -1107,7 +1111,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub pw.println("StartInput #" + entry.mSequenceNumber + ":"); pw.print(prefix); - pw.println(" time=" + dataFormat.format(new Date(entry.mWallTime)) + pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime)) + " (timestamp=" + entry.mTimestamp + ")" + " reason=" + InputMethodDebug.startInputReasonToString(entry.mStartInputReason) @@ -1149,6 +1153,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private final SoftInputShowHideHistory mSoftInputShowHideHistory = new SoftInputShowHideHistory(); + @NonNull + private final ImeTrackerService mImeTrackerService = new ImeTrackerService(); + class SettingsObserver extends ContentObserver { int mUserId; boolean mRegistered = false; @@ -3405,13 +3412,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { // Create statsToken is none exists. if (statsToken == null) { - String packageName = null; - if (mCurEditorInfo != null) { - packageName = mCurEditorInfo.packageName; - } - statsToken = new ImeTracker.Token(packageName); - ImeTracker.get().onRequestShow(statsToken, ImeTracker.ORIGIN_SERVER_START_INPUT, - reason); + // TODO(b/261565259): to avoid using null, add package name in ClientState + final String packageName = (mCurEditorInfo != null) ? mCurEditorInfo.packageName : null; + final int uid = mCurClient != null ? mCurClient.mUid : -1; + statsToken = ImeTracker.get().onRequestShow(packageName, uid, + ImeTracker.ORIGIN_SERVER_START_INPUT, reason); } mShowRequested = true; @@ -3461,7 +3466,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub InputMethodDebug.softInputDisplayReasonToString(reason), InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); } - onShowHideSoftInputRequested(true /* show */, windowToken, reason); + onShowHideSoftInputRequested(true /* show */, windowToken, reason, statsToken); } mInputShown = true; return true; @@ -3508,12 +3513,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { // Create statsToken is none exists. if (statsToken == null) { - String packageName = null; - if (mCurEditorInfo != null) { - packageName = mCurEditorInfo.packageName; + // TODO(b/261565259): to avoid using null, add package name in ClientState + final String packageName = (mCurEditorInfo != null) ? mCurEditorInfo.packageName : null; + final int uid; + if (mCurClient != null) { + uid = mCurClient.mUid; + } else if (mCurFocusedWindowClient != null) { + uid = mCurFocusedWindowClient.mUid; + } else { + uid = -1; } - statsToken = new ImeTracker.Token(packageName); - ImeTracker.get().onRequestHide(statsToken, ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason); + statsToken = ImeTracker.get().onRequestHide(packageName, uid, + ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason); } if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 @@ -3565,7 +3576,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub InputMethodDebug.softInputDisplayReasonToString(reason), InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); } - onShowHideSoftInputRequested(false /* show */, windowToken, reason); + onShowHideSoftInputRequested(false /* show */, windowToken, reason, statsToken); } res = true; } else { @@ -4781,6 +4792,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.applyImeVisibility"); synchronized (ImfLock.class) { if (!calledWithValidTokenLocked(token)) { + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); return; } if (!setVisible) { @@ -4846,7 +4858,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** Called right after {@link com.android.internal.inputmethod.IInputMethod#showSoftInput}. */ @GuardedBy("ImfLock.class") private void onShowHideSoftInputRequested(boolean show, IBinder requestToken, - @SoftInputShowHideReason int reason) { + @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) { final WindowManagerInternal.ImeTargetInfo info = mWindowManagerInternal.onToggleImeRequested( show, mCurFocusedWindow, requestToken, mCurTokenDisplayId); @@ -4855,6 +4867,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurFocusedWindowSoftInputMode, reason, mInFullscreenMode, info.requestWindowName, info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName)); + + mImeTrackerService.onImmsUpdate(statsToken.mBinder, info.requestWindowName); } @BinderThread @@ -5994,6 +6008,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub p.println(" mSoftInputShowHideHistory:"); mSoftInputShowHideHistory.dump(pw, " "); + + p.println(" mImeTrackerService#History:"); + mImeTrackerService.dump(pw, " "); } // Exit here for critical dump, as remaining sections require IPCs to other processes. @@ -6584,6 +6601,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return true; } + /** @hide */ + @Override + public IImeTracker getImeTrackerService() { + return mImeTrackerService; + } + private static final class InputMethodPrivilegedOperationsImpl extends IInputMethodPrivilegedOperations.Stub { private final InputMethodManagerService mImms;