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);
         }
     }