Track IME user requests explicitly
This adds a from_user property to the ImeRequestFinished atom, to allow
tracking IME requests that come directly as a result of user
interaction, as these are more important metrics-wise.
Test: android.view.inputmethod.cts.InputMethodStatsTest
Bug: 303041796
Change-Id: I93f6262912b3a28a3c32d819560fb54b7b1d9251
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 4a3b8ac..de809c8 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -168,7 +168,8 @@
statsToken = ImeTracker.forLogging().onRequestHide(null /* component */,
Process.myUid(),
ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
- SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API,
+ mController.getHost().isHandlingPointerEvent() /* fromUser */);
}
ImeTracker.forLogging().onProgress(statsToken,
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index dd09157..6b7f9db 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -224,6 +224,11 @@
* @param running {@code true} if there is any animation running; {@code false} otherwise.
*/
default void notifyAnimationRunningStateChanged(boolean running) {}
+
+ /** @see ViewRootImpl#isHandlingPointerEvent */
+ default boolean isHandlingPointerEvent() {
+ return false;
+ }
}
private static final String TAG = "InsetsController";
@@ -1063,7 +1068,8 @@
if ((types & ime()) != 0) {
statsToken = ImeTracker.forLogging().onRequestShow(null /* component */,
Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
- SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API);
+ SoftInputShowHideReason.SHOW_SOFT_INPUT_BY_INSETS_API,
+ mHost.isHandlingPointerEvent() /* fromUser */);
}
show(types, false /* fromIme */, statsToken);
@@ -1168,7 +1174,8 @@
if ((types & ime()) != 0) {
statsToken = ImeTracker.forLogging().onRequestHide(null /* component */,
Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
- SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API);
+ SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API,
+ mHost.isHandlingPointerEvent() /* fromUser */);
}
hide(types, false /* fromIme */, statsToken);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7bc832e..5ad08d3 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -7542,6 +7542,15 @@
}
}
+ /**
+ * Returns whether this view is currently handling a pointer event.
+ *
+ * @hide
+ */
+ public boolean isHandlingPointerEvent() {
+ return mAttachInfo.mHandlingPointerEvent;
+ }
+
private void resetPointerIcon(MotionEvent event) {
mPointerIconType = null;
mResolvedPointerIcon = null;
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index 40730e8..f2a3b4c 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -286,6 +286,11 @@
}
}
+ @Override
+ public boolean isHandlingPointerEvent() {
+ return mViewRoot != null && mViewRoot.isHandlingPointerEvent();
+ }
+
private boolean isVisibleToUser() {
return mViewRoot.getHostVisibility() == View.VISIBLE;
}
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index c244287..89da041 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -575,14 +575,14 @@
@AnyThread
@NonNull
static ImeTracker.Token onRequestShow(@NonNull String tag, int uid,
- @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) {
final IImeTracker service = getImeTrackerService();
if (service == null) {
// Create token with "fake" binder if the service was not found.
return new ImeTracker.Token(new Binder(), tag);
}
try {
- return service.onRequestShow(tag, uid, origin, reason);
+ return service.onRequestShow(tag, uid, origin, reason, fromUser);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -592,14 +592,14 @@
@AnyThread
@NonNull
static ImeTracker.Token onRequestHide(@NonNull String tag, int uid,
- @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) {
final IImeTracker service = getImeTrackerService();
if (service == null) {
// Create token with "fake" binder if the service was not found.
return new ImeTracker.Token(new Binder(), tag);
}
try {
- return service.onRequestHide(tag, uid, origin, reason);
+ return service.onRequestHide(tag, uid, origin, reason, fromUser);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index 1b7d57b..31c0363 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -37,6 +37,7 @@
import android.util.Log;
import android.view.InsetsController.AnimationType;
import android.view.SurfaceControl;
+import android.view.View;
import com.android.internal.inputmethod.InputMethodDebug;
import com.android.internal.inputmethod.SoftInputShowHideReason;
@@ -325,12 +326,22 @@
* @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.
+ * @param fromUser whether this request was created directly from user interaction.
*
* @return An IME tracking token.
*/
@NonNull
Token onRequestShow(@Nullable String component, int uid, @Origin int origin,
- @SoftInputShowHideReason int reason);
+ @SoftInputShowHideReason int reason, boolean fromUser);
+
+ /**
+ * Alias for {@link #onRequestShow(String, int, int, int, boolean)} with
+ * {@code fromUser} set to {@code false}.
+ */
+ default Token onRequestShow(@Nullable String component, int uid, @Origin int origin,
+ @SoftInputShowHideReason int reason) {
+ return onRequestShow(component, uid, origin, reason, false /* fromUser */);
+ }
/**
* Creates an IME hide request tracking token.
@@ -340,12 +351,22 @@
* @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.
+ * @param fromUser whether this request was created directly from user interaction.
*
* @return An IME tracking token.
*/
@NonNull
Token onRequestHide(@Nullable String component, int uid, @Origin int origin,
- @SoftInputShowHideReason int reason);
+ @SoftInputShowHideReason int reason, boolean fromUser);
+
+ /**
+ * Alias for {@link #onRequestHide(String, int, int, int, boolean)} with
+ * {@code fromUser} set to {@code false}.
+ */
+ default Token onRequestHide(@Nullable String component, int uid, @Origin int origin,
+ @SoftInputShowHideReason int reason) {
+ return onRequestHide(component, uid, origin, reason, false /* fromUser */);
+ }
/**
* Called when an IME request progresses to a further phase.
@@ -394,6 +415,28 @@
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
@@ -450,13 +493,14 @@
@NonNull
@Override
public Token onRequestShow(@Nullable String component, int uid, @Origin int origin,
- @SoftInputShowHideReason int reason) {
+ @SoftInputShowHideReason int reason, boolean fromUser) {
final var tag = getTag(component);
final var token = IInputMethodManagerGlobalInvoker.onRequestShow(tag, uid, origin,
- reason);
+ reason, fromUser);
Log.i(TAG, token.mTag + ": onRequestShow at " + Debug.originToString(origin)
- + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason),
+ + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason)
+ + " fromUser " + fromUser,
mLogStackTrace ? new Throwable() : null);
return token;
@@ -465,13 +509,14 @@
@NonNull
@Override
public Token onRequestHide(@Nullable String component, int uid, @Origin int origin,
- @SoftInputShowHideReason int reason) {
+ @SoftInputShowHideReason int reason, boolean fromUser) {
final var tag = getTag(component);
final var token = IInputMethodManagerGlobalInvoker.onRequestHide(tag, uid, origin,
- reason);
+ reason, fromUser);
Log.i(TAG, token.mTag + ": onRequestHide at " + Debug.originToString(origin)
- + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason),
+ + " reason " + InputMethodDebug.softInputDisplayReasonToString(reason)
+ + " fromUser " + fromUser,
mLogStackTrace ? new Throwable() : null);
return token;
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 3bc02a6..3d70c5b 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2141,8 +2141,10 @@
@ShowFlags int flags, ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason) {
if (statsToken == null) {
+ // TODO(b/303041796): handle tracking physical keyboard and DPAD as user interactions
statsToken = ImeTracker.forLogging().onRequestShow(null /* component */,
- Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason);
+ Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason,
+ ImeTracker.isFromUser(view));
}
ImeTracker.forLatency().onRequestShow(statsToken, ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
reason, ActivityThread::currentApplication);
@@ -2291,15 +2293,22 @@
private boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+ // Get served view initially for statsToken creation.
+ final View initialServedView;
+ synchronized (mH) {
+ initialServedView = getServedViewLocked();
+ }
+
final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide(
- null /* component */, Process.myUid(),
- ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, reason);
+ null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ reason, ImeTracker.isFromUser(initialServedView));
ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
reason, ActivityThread::currentApplication);
ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromWindow",
this, null /* icProto */);
checkFocus();
synchronized (mH) {
+ // Get served view again in case it changed between the synchronized blocks.
final View servedView = getServedViewLocked();
if (servedView == null || servedView.getWindowToken() != windowToken) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
@@ -2335,8 +2344,8 @@
final var reason = SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_VIEW;
final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide(
- null /* component */, Process.myUid(),
- ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, reason);
+ null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
+ reason, ImeTracker.isFromUser(view));
ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT,
reason, ActivityThread::currentApplication);
ImeTracing.getInstance().triggerClientDump("InputMethodManager#hideSoftInputFromView",
diff --git a/core/java/com/android/internal/inputmethod/IImeTracker.aidl b/core/java/com/android/internal/inputmethod/IImeTracker.aidl
index c7418ee..2759043 100644
--- a/core/java/com/android/internal/inputmethod/IImeTracker.aidl
+++ b/core/java/com/android/internal/inputmethod/IImeTracker.aidl
@@ -31,9 +31,10 @@
* @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.
+ * @param fromUser whether this request was created directly from user interaction.
* @return A new IME tracking token.
*/
- ImeTracker.Token onRequestShow(String tag, int uid, int origin, int reason);
+ ImeTracker.Token onRequestShow(String tag, int uid, int origin, int reason, boolean fromUser);
/**
* Called when an IME hide request is created.
@@ -42,9 +43,10 @@
* @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.
+ * @param fromUser whether this request was created directly from user interaction.
* @return A new IME tracking token.
*/
- ImeTracker.Token onRequestHide(String tag, int uid, int origin, int reason);
+ ImeTracker.Token onRequestHide(String tag, int uid, int origin, int reason, boolean fromUser);
/**
* Called when the IME request progresses to a further phase.
diff --git a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
index 27e1b9a..d06c31c 100644
--- a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
+++ b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
@@ -76,11 +76,11 @@
@NonNull
@Override
public ImeTracker.Token onRequestShow(@NonNull String tag, int uid,
- @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) {
final var binder = new Binder();
final var token = new ImeTracker.Token(binder, tag);
final var entry = new History.Entry(tag, uid, ImeTracker.TYPE_SHOW, ImeTracker.STATUS_RUN,
- origin, reason);
+ origin, reason, fromUser);
synchronized (mLock) {
mHistory.addEntry(binder, entry);
@@ -98,11 +98,11 @@
@NonNull
@Override
public ImeTracker.Token onRequestHide(@NonNull String tag, int uid,
- @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) {
final var binder = new Binder();
final var token = new ImeTracker.Token(binder, tag);
final var entry = new History.Entry(tag, uid, ImeTracker.TYPE_HIDE, ImeTracker.STATUS_RUN,
- origin, reason);
+ origin, reason, fromUser);
synchronized (mLock) {
mHistory.addEntry(binder, entry);
@@ -269,7 +269,7 @@
// Log newly finished entry.
FrameworkStatsLog.write(FrameworkStatsLog.IME_REQUEST_FINISHED, entry.mUid,
entry.mDuration, entry.mType, entry.mStatus, entry.mReason,
- entry.mOrigin, entry.mPhase);
+ entry.mOrigin, entry.mPhase, entry.mFromUser);
}
/** Dumps the contents of the circular buffer. */
@@ -353,6 +353,9 @@
@ImeTracker.Phase
private int mPhase = ImeTracker.PHASE_NOT_SET;
+ /** Whether this request was created directly from a user interaction. */
+ private final boolean mFromUser;
+
/**
* Name of the window that created the IME request.
*
@@ -363,13 +366,14 @@
private Entry(@NonNull String tag, int uid, @ImeTracker.Type int type,
@ImeTracker.Status int status, @ImeTracker.Origin int origin,
- @SoftInputShowHideReason int reason) {
+ @SoftInputShowHideReason int reason, boolean fromUser) {
mTag = tag;
mUid = uid;
mType = type;
mStatus = status;
mOrigin = origin;
mReason = reason;
+ mFromUser = fromUser;
}
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f031b7b..54b0615 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -3514,7 +3514,7 @@
// Create statsToken is none exists.
if (statsToken == null) {
statsToken = createStatsTokenForFocusedClient(true /* show */,
- ImeTracker.ORIGIN_SERVER_START_INPUT, reason);
+ ImeTracker.ORIGIN_SERVER_START_INPUT, reason, false /* fromUser */);
}
if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) {
@@ -3590,8 +3590,9 @@
@SoftInputShowHideReason int reason) {
// Create statsToken is none exists.
if (statsToken == null) {
+ final boolean fromUser = reason == SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_BACK_KEY;
statsToken = createStatsTokenForFocusedClient(false /* show */,
- ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason);
+ ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason, fromUser);
}
if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) {
@@ -6639,10 +6640,11 @@
* @param show whether this is a show or a hide request.
* @param origin the origin of the IME request.
* @param reason the reason why the IME request was created.
+ * @param fromUser whether this request was created directly from user interaction.
*/
@NonNull
private ImeTracker.Token createStatsTokenForFocusedClient(boolean show,
- @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
+ @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason, boolean fromUser) {
final int uid = mCurFocusedWindowClient != null
? mCurFocusedWindowClient.mUid
: -1;
@@ -6651,9 +6653,11 @@
: "uid(" + uid + ")";
if (show) {
- return ImeTracker.forLogging().onRequestShow(packageName, uid, origin, reason);
+ return ImeTracker.forLogging()
+ .onRequestShow(packageName, uid, origin, reason, fromUser);
} else {
- return ImeTracker.forLogging().onRequestHide(packageName, uid, origin, reason);
+ return ImeTracker.forLogging()
+ .onRequestHide(packageName, uid, origin, reason, fromUser);
}
}