/* * 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 android.view.inputmethod; import static android.view.InsetsController.ANIMATION_TYPE_HIDE; import static android.view.InsetsController.ANIMATION_TYPE_SHOW; import static com.android.internal.inputmethod.InputMethodDebug.softInputDisplayReasonToString; import static com.android.internal.jank.Cuj.CUJ_IME_INSETS_HIDE_ANIMATION; import static com.android.internal.jank.Cuj.CUJ_IME_INSETS_SHOW_ANIMATION; import static com.android.internal.util.LatencyTracker.ACTION_REQUEST_IME_HIDDEN; import static com.android.internal.util.LatencyTracker.ACTION_REQUEST_IME_SHOWN; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.Process; import android.os.SystemProperties; import android.util.Log; import android.view.InsetsController.AnimationType; import android.view.SurfaceControl; import android.view.View; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.inputmethod.InputMethodDebug; import com.android.internal.inputmethod.SoftInputShowHideReason; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.internal.util.LatencyTracker; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Locale; import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; /** @hide */ public interface ImeTracker { String TAG = "ImeTracker"; /** The debug flag for IME visibility event log. */ boolean DEBUG_IME_VISIBILITY = SystemProperties.getBoolean("persist.debug.imf_event", false); /** The message to indicate if there is no valid {@link Token}. */ String TOKEN_NONE = "TOKEN_NONE"; /** 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 * *

The name follows the format {@code ORIGIN_x_...} where {@code x} denotes * where the origin is (i.e. {@code ORIGIN_SERVER} occurs in the server). */ @IntDef(prefix = { "ORIGIN_" }, value = { ORIGIN_CLIENT, ORIGIN_SERVER, ORIGIN_IME }) @Retention(RetentionPolicy.SOURCE) @interface Origin {} /** The IME request originated in the client. */ int ORIGIN_CLIENT = ImeProtoEnums.ORIGIN_CLIENT; /** The IME request originated in the server. */ int ORIGIN_SERVER = ImeProtoEnums.ORIGIN_SERVER; /** The IME request originated in the IME. */ int ORIGIN_IME = ImeProtoEnums.ORIGIN_IME; /** The IME request originated in the WindowManager Shell. */ int ORIGIN_WM_SHELL = ImeProtoEnums.ORIGIN_WM_SHELL; /** * The current phase of the IME request. * *

The name follows the format {@code PHASE_x_...} where {@code x} denotes * 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, PHASE_SERVER_ACCESSIBILITY, PHASE_SERVER_SYSTEM_READY, PHASE_SERVER_HIDE_IMPLICIT, PHASE_SERVER_HIDE_NOT_ALWAYS, PHASE_SERVER_WAIT_IME, PHASE_SERVER_HAS_IME, PHASE_SERVER_SHOULD_HIDE, PHASE_IME_WRAPPER, PHASE_IME_WRAPPER_DISPATCH, PHASE_IME_SHOW_SOFT_INPUT, PHASE_IME_HIDE_SOFT_INPUT, PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE, PHASE_SERVER_APPLY_IME_VISIBILITY, PHASE_WM_SHOW_IME_RUNNER, PHASE_WM_SHOW_IME_READY, PHASE_WM_HAS_IME_INSETS_CONTROL_TARGET, PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_SHOW_INSETS, PHASE_WM_WINDOW_INSETS_CONTROL_TARGET_HIDE_INSETS, PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_SHOW_INSETS, PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS, PHASE_WM_REMOTE_INSETS_CONTROLLER, PHASE_WM_ANIMATION_CREATE, PHASE_WM_ANIMATION_RUNNING, PHASE_CLIENT_SHOW_INSETS, PHASE_CLIENT_HIDE_INSETS, PHASE_CLIENT_HANDLE_SHOW_INSETS, PHASE_CLIENT_HANDLE_HIDE_INSETS, PHASE_CLIENT_APPLY_ANIMATION, PHASE_CLIENT_CONTROL_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, PHASE_CLIENT_ANIMATION_FINISHED_HIDE, PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT, PHASE_IME_SHOW_WINDOW, PHASE_IME_HIDE_WINDOW, PHASE_IME_PRIVILEGED_OPERATIONS, PHASE_SERVER_CURRENT_ACTIVE_IME, }) @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 = ImeProtoEnums.PHASE_CLIENT_VIEW_SERVED; /** The IME client that requested the IME has window manager focus. */ 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 = ImeProtoEnums.PHASE_SERVER_CLIENT_FOCUSED; /** The IME request complies with the current accessibility settings. */ int PHASE_SERVER_ACCESSIBILITY = ImeProtoEnums.PHASE_SERVER_ACCESSIBILITY; /** The server is ready to run third party code. */ 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 = ImeProtoEnums.PHASE_SERVER_HIDE_IMPLICIT; /** Checked the not-always hide request against any forced show requests. */ 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 = ImeProtoEnums.PHASE_SERVER_WAIT_IME; /** The server has a connection to the IME. */ int PHASE_SERVER_HAS_IME = ImeProtoEnums.PHASE_SERVER_HAS_IME; /** The server decided the IME should be hidden. */ int PHASE_SERVER_SHOULD_HIDE = ImeProtoEnums.PHASE_SERVER_SHOULD_HIDE; /** Reached the IME wrapper. */ int PHASE_IME_WRAPPER = ImeProtoEnums.PHASE_IME_WRAPPER; /** Dispatched from the IME wrapper to the IME. */ int PHASE_IME_WRAPPER_DISPATCH = ImeProtoEnums.PHASE_IME_WRAPPER_DISPATCH; /** Reached the IME's showSoftInput method. */ int PHASE_IME_SHOW_SOFT_INPUT = ImeProtoEnums.PHASE_IME_SHOW_SOFT_INPUT; /** Reached the IME's hideSoftInput method. */ 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 = ImeProtoEnums.PHASE_IME_ON_SHOW_SOFT_INPUT_TRUE; /** Applied the IME visibility. */ int PHASE_SERVER_APPLY_IME_VISIBILITY = ImeProtoEnums.PHASE_SERVER_APPLY_IME_VISIBILITY; /** Started the show IME runner. */ int PHASE_WM_SHOW_IME_RUNNER = ImeProtoEnums.PHASE_WM_SHOW_IME_RUNNER; /** Ready to show IME. */ 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 = 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 = 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 = 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 = 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 = ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROL_TARGET_HIDE_INSETS; /** Reached the remote insets controller. */ int PHASE_WM_REMOTE_INSETS_CONTROLLER = ImeProtoEnums.PHASE_WM_REMOTE_INSETS_CONTROLLER; /** Created the IME window insets show animation. */ int PHASE_WM_ANIMATION_CREATE = ImeProtoEnums.PHASE_WM_ANIMATION_CREATE; /** Started the IME window insets show animation. */ int PHASE_WM_ANIMATION_RUNNING = ImeProtoEnums.PHASE_WM_ANIMATION_RUNNING; /** Reached the client's show insets method. */ int PHASE_CLIENT_SHOW_INSETS = ImeProtoEnums.PHASE_CLIENT_SHOW_INSETS; /** Reached the client's hide insets method. */ int PHASE_CLIENT_HIDE_INSETS = ImeProtoEnums.PHASE_CLIENT_HIDE_INSETS; /** Handling the IME window insets show request. */ 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 = ImeProtoEnums.PHASE_CLIENT_HANDLE_HIDE_INSETS; /** Applied the IME window insets show animation. */ int PHASE_CLIENT_APPLY_ANIMATION = ImeProtoEnums.PHASE_CLIENT_APPLY_ANIMATION; /** Started the IME window insets show animation. */ int PHASE_CLIENT_CONTROL_ANIMATION = ImeProtoEnums.PHASE_CLIENT_CONTROL_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 = ImeProtoEnums.PHASE_CLIENT_ANIMATION_RUNNING; /** Cancelled the IME window insets show animation. */ int PHASE_CLIENT_ANIMATION_CANCEL = ImeProtoEnums.PHASE_CLIENT_ANIMATION_CANCEL; /** Finished the IME window insets show animation. */ 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 = ImeProtoEnums.PHASE_CLIENT_ANIMATION_FINISHED_HIDE; /** Aborted the request to show the IME post layout. */ int PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT = ImeProtoEnums.PHASE_WM_ABORT_SHOW_IME_POST_LAYOUT; /** Reached the IME's showWindow method. */ int PHASE_IME_SHOW_WINDOW = ImeProtoEnums.PHASE_IME_SHOW_WINDOW; /** Reached the IME's hideWindow method. */ int PHASE_IME_HIDE_WINDOW = ImeProtoEnums.PHASE_IME_HIDE_WINDOW; /** Reached the InputMethodPrivilegedOperations handler. */ int PHASE_IME_PRIVILEGED_OPERATIONS = ImeProtoEnums.PHASE_IME_PRIVILEGED_OPERATIONS; /** Checked that the calling IME is the currently active IME. */ int PHASE_SERVER_CURRENT_ACTIVE_IME = ImeProtoEnums.PHASE_SERVER_CURRENT_ACTIVE_IME; /** * Called when an IME request is started. * * @param component the name of the component that started the request. * @param uid the uid of the client that started the request. * @param type the type of the request. * @param origin the origin of the request. * @param reason the reason for starting the request. * @param fromUser whether this request was created directly from user interaction. * * @return An IME request tracking token. */ @NonNull Token onStart(@NonNull String component, int uid, @Type int type, @Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser); /** * Called when an IME request is started for the current process. * * @param type the type of the request. * @param origin the origin of the request. * @param reason the reason for starting the request. * @param fromUser whether this request was created directly from user interaction. * * @return An IME request tracking token. */ @NonNull default Token onStart(@Type int type, @Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) { return onStart(Process.myProcessName(), Process.myUid(), type, origin, reason, fromUser); } /** * Called when an IME request progresses to a further phase. * * @param token the token tracking the current IME request or {@code null} otherwise. * @param phase the new phase the IME request reached. */ void onProgress(@Nullable Token token, @Phase int phase); /** * Called when an IME request fails. * * @param token the token tracking the current IME request or {@code null} otherwise. * @param phase the phase the IME request failed at. */ void onFailed(@Nullable Token token, @Phase int phase); /** * Called when an IME request reached a flow that is not yet implemented. * * @param token the token tracking the current IME request or {@code null} otherwise. * @param phase the phase the IME request was currently at. */ void onTodo(@Nullable Token token, @Phase int phase); /** * Called when an IME request is cancelled. * * @param token the token tracking the current IME request or {@code null} otherwise. * @param phase the phase the IME request was cancelled at. */ void onCancelled(@Nullable Token token, @Phase int phase); /** * Called when the IME show request is successful. * * @param token the token tracking the current IME request or {@code null} otherwise. */ void onShown(@Nullable Token token); /** * Called when the IME hide request is successful. * * @param token the token tracking the current IME request or {@code null} otherwise. */ void onHidden(@Nullable Token token); /** * Returns whether the current IME request was created due to a user interaction. This can * only be {@code true} when running on the view's UI thread. * * @param view the view for which the IME was requested. * @return {@code true} if this request is coming from a user interaction, * {@code false} otherwise. */ static boolean isFromUser(@Nullable View view) { if (view == null) { return false; } final var handler = view.getHandler(); // Early return if not on the UI thread, to ensure safe access to getViewRootImpl() below. if (handler == null || handler.getLooper() == null || !handler.getLooper().isCurrentThread()) { return false; } final var viewRootImpl = view.getViewRootImpl(); return viewRootImpl != null && viewRootImpl.isHandlingPointerEvent(); } /** * Get the singleton request tracker instance. * * @return the singleton request tracker instance */ @NonNull static ImeTracker forLogging() { return LOGGER; } /** * Get the singleton jank tracker instance. * * @return the singleton jank tracker instance */ @NonNull static ImeJankTracker forJank() { return JANK_TRACKER; } /** * Get the singleton latency tracker instance. * * @return the singleton latency tracker instance */ @NonNull static ImeLatencyTracker forLatency() { return LATENCY_TRACKER; } /** The singleton IME tracker instance. */ @NonNull ImeTracker LOGGER = new ImeTracker() { { // Read initial system properties. reloadSystemProperties(); // Update when system properties change. SystemProperties.addChangeCallback(this::reloadSystemProperties); } /** Whether {@link #onProgress} calls should be logged. */ private boolean mLogProgress; /** Whether the stack trace at the request call site should be logged. */ private boolean mLogStackTrace; private void reloadSystemProperties() { mLogProgress = SystemProperties.getBoolean( "persist.debug.imetracker", false); mLogStackTrace = SystemProperties.getBoolean( "persist.debug.imerequest.logstacktrace", false); } @NonNull @Override public Token onStart(@NonNull String component, int uid, @Type int type, @Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) { final var tag = Token.createTag(component); final var token = IInputMethodManagerGlobalInvoker.onStart(tag, uid, type, origin, reason, fromUser); Log.i(TAG, token.mTag + ": onRequest" + (type == TYPE_SHOW ? "Show" : "Hide") + " at " + Debug.originToString(origin) + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason) + " fromUser " + fromUser, mLogStackTrace ? new Throwable() : null); return token; } @Override public void onProgress(@Nullable Token token, @Phase int phase) { if (token == null) return; IInputMethodManagerGlobalInvoker.onProgress(token.mBinder, phase); if (mLogProgress) { Log.i(TAG, token.mTag + ": onProgress at " + Debug.phaseToString(phase)); } } @Override public void onFailed(@Nullable Token token, @Phase int phase) { if (token == null) return; IInputMethodManagerGlobalInvoker.onFailed(token, phase); Log.i(TAG, token.mTag + ": onFailed at " + Debug.phaseToString(phase)); } @Override public void onTodo(@Nullable Token token, @Phase int phase) { if (token == null) return; Log.i(TAG, token.mTag + ": onTodo at " + Debug.phaseToString(phase)); } @Override public void onCancelled(@Nullable Token token, @Phase int phase) { if (token == null) return; IInputMethodManagerGlobalInvoker.onCancelled(token, 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); Log.i(TAG, token.mTag + ": onShown"); } @Override public void onHidden(@Nullable Token token) { if (token == null) return; IInputMethodManagerGlobalInvoker.onHidden(token); Log.i(TAG, token.mTag + ": onHidden"); } }; /** The singleton IME tracker instance for instrumenting jank metrics. */ ImeJankTracker JANK_TRACKER = new ImeJankTracker(); /** The singleton IME tracker instance for instrumenting latency metrics. */ ImeLatencyTracker LATENCY_TRACKER = new ImeLatencyTracker(); /** A token that tracks the progress of an IME request. */ final class Token implements Parcelable { /** Empty binder, lazily initialized, used for empty token instantiation. */ @Nullable private static IBinder sEmptyBinder; /** The binder used to identify this token. */ @NonNull private final IBinder mBinder; /** Logging tag, of the shape "component:random_hexadecimal". */ @NonNull private final String mTag; public Token(@NonNull IBinder binder, @NonNull String tag) { mBinder = binder; mTag = tag; } private Token(@NonNull Parcel in) { mBinder = in.readStrongBinder(); mTag = in.readString8(); } /** Returns the binder used to identify this token. */ @NonNull public IBinder getBinder() { return mBinder; } /** Returns the logging tag of this token. */ @NonNull public String getTag() { return mTag; } /** * Creates a logging tag. * * @param component the name of the component that created the IME request. */ @NonNull private static String createTag(@NonNull String component) { return component + ":" + Integer.toHexString(ThreadLocalRandom.current().nextInt()); } /** Returns a new token with an empty binder. */ @NonNull @VisibleForTesting(visibility = Visibility.PACKAGE) public static Token empty() { final var tag = createTag(Process.myProcessName()); return empty(tag); } /** Returns a new token with an empty binder and the given logging tag. */ @NonNull static Token empty(@NonNull String tag) { return new Token(getEmptyBinder(), tag); } /** Returns the empty binder instance for empty token creation, lazily initializing it. */ @NonNull private static IBinder getEmptyBinder() { if (sEmptyBinder == null) { sEmptyBinder = new Binder(); } return sEmptyBinder; } @Override public String toString() { return super.toString() + "(tag: " + mTag + ")"; } /** For Parcelable, no special marshalled objects. */ @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeStrongBinder(mBinder); dest.writeString8(mTag); } @NonNull public static final Creator CREATOR = new Creator<>() { @NonNull @Override public Token createFromParcel(@NonNull Parcel in) { return new Token(in); } @NonNull @Override public Token[] newArray(int size) { return new Token[size]; } }; } /** * 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. */ final class Debug { @NonNull private static final Map sTypes = getFieldMapping(ImeTracker.class, "TYPE_"); @NonNull private static final Map sStatus = getFieldMapping(ImeTracker.class, "STATUS_"); @NonNull private static final Map sOrigins = getFieldMapping(ImeTracker.class, "ORIGIN_"); @NonNull private static final Map sPhases = getFieldMapping(ImeTracker.class, "PHASE_"); @NonNull public static String typeToString(@Type int type) { return sTypes.getOrDefault(type, "TYPE_" + type); } @NonNull public static String statusToString(@Status int status) { return sStatus.getOrDefault(status, "STATUS_" + status); } @NonNull public static String originToString(@Origin int origin) { return sOrigins.getOrDefault(origin, "ORIGIN_" + origin); } @NonNull public static String phaseToString(@Phase int phase) { return sPhases.getOrDefault(phase, "PHASE_" + phase); } @NonNull private static Map getFieldMapping(Class cls, @NonNull String fieldPrefix) { return Arrays.stream(cls.getDeclaredFields()) .filter(field -> field.getName().startsWith(fieldPrefix)) .collect(Collectors.toMap(Debug::getFieldValue, Field::getName)); } private static int getFieldValue(@NonNull Field field) { try { return field.getInt(null); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } /** * Context related to {@link InteractionJankMonitor}. */ interface InputMethodJankContext { /** * @return a context associated with a display */ Context getDisplayContext(); /** * @return a SurfaceControl that is going to be monitored */ SurfaceControl getTargetSurfaceControl(); /** * @return the package name of the host */ String getHostPackageName(); } /** * Context related to {@link LatencyTracker}. */ interface InputMethodLatencyContext { /** * @return a context associated with current application */ Context getAppContext(); } /** * A tracker instance which is in charge of communicating with {@link InteractionJankMonitor}. * This class disallows instantiating from outside, use {@link #forJank()} to get the singleton. */ final class ImeJankTracker { /** * This class disallows instantiating from outside. */ private ImeJankTracker() { } /** * Called when the animation, which is going to be monitored, starts. * * @param jankContext context which is needed by {@link InteractionJankMonitor}. * @param animType the animation type. * @param useSeparatedThread {@code true} if the animation is handled by the app, * {@code false} if the animation will be scheduled on the * {@link android.view.InsetsAnimationThread}. */ public void onRequestAnimation(@NonNull InputMethodJankContext jankContext, @AnimationType int animType, boolean useSeparatedThread) { final int cujType = getImeInsetsCujFromAnimation(animType); if (jankContext.getDisplayContext() == null || jankContext.getTargetSurfaceControl() == null || cujType == -1) { return; } final Configuration.Builder builder = Configuration.Builder.withSurface( cujType, jankContext.getDisplayContext(), jankContext.getTargetSurfaceControl()) .setTag(String.format(Locale.US, "%d@%d@%s", animType, useSeparatedThread ? 0 : 1, jankContext.getHostPackageName())); InteractionJankMonitor.getInstance().begin(builder); } /** * Called when the animation, which is going to be monitored, cancels. * * @param animType the animation type. */ public void onCancelAnimation(@AnimationType int animType) { final int cujType = getImeInsetsCujFromAnimation(animType); if (cujType != -1) { InteractionJankMonitor.getInstance().cancel(cujType); } } /** * Called when the animation, which is going to be monitored, ends. * * @param animType the animation type. */ public void onFinishAnimation(@AnimationType int animType) { final int cujType = getImeInsetsCujFromAnimation(animType); if (cujType != -1) { InteractionJankMonitor.getInstance().end(cujType); } } /** * A helper method to translate animation type to CUJ type for IME animations. * * @param animType the animation type. * @return the integer in {@link com.android.internal.jank.Cuj.CujType}, * or {@code -1} if the animation type is not supported for tracking yet. */ private static int getImeInsetsCujFromAnimation(@AnimationType int animType) { switch (animType) { case ANIMATION_TYPE_SHOW: return CUJ_IME_INSETS_SHOW_ANIMATION; case ANIMATION_TYPE_HIDE: return CUJ_IME_INSETS_HIDE_ANIMATION; default: return -1; } } } /** * A tracker instance which is in charge of communicating with {@link LatencyTracker}. * This class disallows instantiating from outside, use {@link #forLatency()} * to get the singleton. */ final class ImeLatencyTracker { /** * This class disallows instantiating from outside. */ private ImeLatencyTracker() { } private boolean shouldMonitorLatency(@SoftInputShowHideReason int reason) { return reason == SoftInputShowHideReason.SHOW_SOFT_INPUT || reason == SoftInputShowHideReason.HIDE_SOFT_INPUT || reason == SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW || reason == SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API || reason == SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API || reason == SoftInputShowHideReason.SHOW_SOFT_INPUT_FROM_IME || reason == SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_IME; } public void onRequestShow(@Nullable Token token, @Origin int origin, @SoftInputShowHideReason int reason, @NonNull InputMethodLatencyContext latencyContext) { if (!shouldMonitorLatency(reason)) return; LatencyTracker.getInstance(latencyContext.getAppContext()) .onActionStart( ACTION_REQUEST_IME_SHOWN, softInputDisplayReasonToString(reason)); } public void onRequestHide(@Nullable Token token, @Origin int origin, @SoftInputShowHideReason int reason, @NonNull InputMethodLatencyContext latencyContext) { if (!shouldMonitorLatency(reason)) return; LatencyTracker.getInstance(latencyContext.getAppContext()) .onActionStart( ACTION_REQUEST_IME_HIDDEN, softInputDisplayReasonToString(reason)); } public void onShowFailed(@Nullable Token token, @Phase int phase, @NonNull InputMethodLatencyContext latencyContext) { onShowCancelled(token, phase, latencyContext); } public void onHideFailed(@Nullable Token token, @Phase int phase, @NonNull InputMethodLatencyContext latencyContext) { onHideCancelled(token, phase, latencyContext); } public void onShowCancelled(@Nullable Token token, @Phase int phase, @NonNull InputMethodLatencyContext latencyContext) { LatencyTracker.getInstance(latencyContext.getAppContext()) .onActionCancel(ACTION_REQUEST_IME_SHOWN); } public void onHideCancelled(@Nullable Token token, @Phase int phase, @NonNull InputMethodLatencyContext latencyContext) { LatencyTracker.getInstance(latencyContext.getAppContext()) .onActionCancel(ACTION_REQUEST_IME_HIDDEN); } public void onShown(@Nullable Token token, @NonNull InputMethodLatencyContext latencyContext) { LatencyTracker.getInstance(latencyContext.getAppContext()) .onActionEnd(ACTION_REQUEST_IME_SHOWN); } public void onHidden(@Nullable Token token, @NonNull InputMethodLatencyContext latencyContext) { LatencyTracker.getInstance(latencyContext.getAppContext()) .onActionEnd(ACTION_REQUEST_IME_HIDDEN); } } }