Introduce pointer capture API.

This depends on I4189eb4d93f50c2865b7a325727be5ceebcc71f8 of
frameworks/native.

Bug: 5452473
Change-Id: Ie21e521f3e5c581f976dc0feb5d84bfa48b046cd
diff --git a/api/current.txt b/api/current.txt
index 5421f05..e39444d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -40244,6 +40244,8 @@
     field public static final int AXIS_LTRIGGER = 17; // 0x11
     field public static final int AXIS_ORIENTATION = 8; // 0x8
     field public static final int AXIS_PRESSURE = 2; // 0x2
+    field public static final int AXIS_RELATIVE_X = 27; // 0x1b
+    field public static final int AXIS_RELATIVE_Y = 28; // 0x1c
     field public static final int AXIS_RTRIGGER = 18; // 0x12
     field public static final int AXIS_RUDDER = 20; // 0x14
     field public static final int AXIS_RX = 12; // 0xc
@@ -40779,6 +40781,7 @@
     method public boolean hasNestedScrollingParent();
     method public boolean hasOnClickListeners();
     method public boolean hasOverlappingRendering();
+    method public boolean hasPointerCapture();
     method public boolean hasTransientState();
     method public boolean hasWindowFocus();
     method public static android.view.View inflate(android.content.Context, int, android.view.ViewGroup);
@@ -40905,6 +40908,7 @@
     method public void postOnAnimation(java.lang.Runnable);
     method public void postOnAnimationDelayed(java.lang.Runnable, long);
     method public void refreshDrawableState();
+    method public void releasePointerCapture();
     method public boolean removeCallbacks(java.lang.Runnable);
     method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
     method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
@@ -41005,6 +41009,7 @@
     method public void setPaddingRelative(int, int, int, int);
     method public void setPivotX(float);
     method public void setPivotY(float);
+    method public void setPointerCapture();
     method public void setPointerIcon(android.view.PointerIcon);
     method public void setPressed(boolean);
     method public final void setRight(int);
diff --git a/api/system-current.txt b/api/system-current.txt
index 3071fd9..e430b9f 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -42599,6 +42599,8 @@
     field public static final int AXIS_LTRIGGER = 17; // 0x11
     field public static final int AXIS_ORIENTATION = 8; // 0x8
     field public static final int AXIS_PRESSURE = 2; // 0x2
+    field public static final int AXIS_RELATIVE_X = 27; // 0x1b
+    field public static final int AXIS_RELATIVE_Y = 28; // 0x1c
     field public static final int AXIS_RTRIGGER = 18; // 0x12
     field public static final int AXIS_RUDDER = 20; // 0x14
     field public static final int AXIS_RX = 12; // 0xc
@@ -43134,6 +43136,7 @@
     method public boolean hasNestedScrollingParent();
     method public boolean hasOnClickListeners();
     method public boolean hasOverlappingRendering();
+    method public boolean hasPointerCapture();
     method public boolean hasTransientState();
     method public boolean hasWindowFocus();
     method public static android.view.View inflate(android.content.Context, int, android.view.ViewGroup);
@@ -43260,6 +43263,7 @@
     method public void postOnAnimation(java.lang.Runnable);
     method public void postOnAnimationDelayed(java.lang.Runnable, long);
     method public void refreshDrawableState();
+    method public void releasePointerCapture();
     method public boolean removeCallbacks(java.lang.Runnable);
     method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
     method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
@@ -43360,6 +43364,7 @@
     method public void setPaddingRelative(int, int, int, int);
     method public void setPivotX(float);
     method public void setPivotY(float);
+    method public void setPointerCapture();
     method public void setPointerIcon(android.view.PointerIcon);
     method public void setPressed(boolean);
     method public final void setRight(int);
diff --git a/api/test-current.txt b/api/test-current.txt
index f418d36..88ddc21 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -40246,6 +40246,8 @@
     field public static final int AXIS_LTRIGGER = 17; // 0x11
     field public static final int AXIS_ORIENTATION = 8; // 0x8
     field public static final int AXIS_PRESSURE = 2; // 0x2
+    field public static final int AXIS_RELATIVE_X = 27; // 0x1b
+    field public static final int AXIS_RELATIVE_Y = 28; // 0x1c
     field public static final int AXIS_RTRIGGER = 18; // 0x12
     field public static final int AXIS_RUDDER = 20; // 0x14
     field public static final int AXIS_RX = 12; // 0xc
@@ -40781,6 +40783,7 @@
     method public boolean hasNestedScrollingParent();
     method public boolean hasOnClickListeners();
     method public boolean hasOverlappingRendering();
+    method public boolean hasPointerCapture();
     method public boolean hasTransientState();
     method public boolean hasWindowFocus();
     method public static android.view.View inflate(android.content.Context, int, android.view.ViewGroup);
@@ -40907,6 +40910,7 @@
     method public void postOnAnimation(java.lang.Runnable);
     method public void postOnAnimationDelayed(java.lang.Runnable, long);
     method public void refreshDrawableState();
+    method public void releasePointerCapture();
     method public boolean removeCallbacks(java.lang.Runnable);
     method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
     method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
@@ -41007,6 +41011,7 @@
     method public void setPaddingRelative(int, int, int, int);
     method public void setPivotX(float);
     method public void setPivotY(float);
+    method public void setPointerCapture();
     method public void setPointerIcon(android.view.PointerIcon);
     method public void setPressed(boolean);
     method public final void setRight(int);
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index ff33bd9..b8f464d 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -73,4 +73,6 @@
 
     void setPointerIconShape(int shapeId);
     void setCustomPointerIcon(in PointerIcon icon);
+
+    void setPointerIconDetached(boolean detached);
 }
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index fab4718..16b8722 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -829,6 +829,24 @@
         }
     }
 
+    /**
+     * Update the pointer icon status. When detached, the pointer icon disappears, and further
+     * mouse location will be stuck at the current point. Mouse movement events will still arrive,
+     * and movement should be handled through {@link MotionEvent.AXIS_RELATIVE_X} and
+     * {@link MotionEvent.AXIS_RELATIVE_Y}.
+     *
+     * @param detached true if the icon will be detached from the actual mouse movement.
+     *
+     * @hide
+     */
+    public void setPointerIconDetached(boolean detached) {
+        try {
+            mIm.setPointerIconDetached(detached);
+        } catch (RemoteException ex) {
+            // Do nothing.
+        }
+    }
+
     private void populateInputDevicesLocked() {
         if (mInputDevicesChangedListener == null) {
             final InputDevicesChangedListener listener = new InputDevicesChangedListener();
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index 0195dec..7a544b8 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -977,6 +977,37 @@
     public static final int AXIS_SCROLL = 26;
 
     /**
+     * Axis constant: The movement of x position of a motion event.
+     * <p>
+     * <ul>
+     * <li>For a mouse, reports a difference of x position between the previous position.
+     * This is useful when pointer is captured, in that case the mouse pointer doesn't change
+     * the location but this axis reports the difference which allows the app to see
+     * how the mouse is moved.
+     * </ul>
+     * </p>
+     *
+     * @see #getAxisValue(int, int)
+     * @see #getHistoricalAxisValue(int, int, int)
+     * @see MotionEvent.PointerCoords#getAxisValue(int, int)
+     * @see InputDevice#getMotionRange
+     */
+    public static final int AXIS_RELATIVE_X = 27;
+
+    /**
+     * Axis constant: The movement of y position of a motion event.
+     * <p>
+     * This is similar to {@link #AXIS_RELATIVE_X} but for y-axis.
+     * </p>
+     *
+     * @see #getAxisValue(int, int)
+     * @see #getHistoricalAxisValue(int, int, int)
+     * @see MotionEvent.PointerCoords#getAxisValue(int, int)
+     * @see InputDevice#getMotionRange
+     */
+    public static final int AXIS_RELATIVE_Y = 28;
+
+    /**
      * Axis constant: Generic 1 axis of a motion event.
      * The interpretation of a generic axis is device-specific.
      *
@@ -1187,6 +1218,8 @@
         names.append(AXIS_DISTANCE, "AXIS_DISTANCE");
         names.append(AXIS_TILT, "AXIS_TILT");
         names.append(AXIS_SCROLL, "AXIS_SCROLL");
+        names.append(AXIS_RELATIVE_X, "AXIS_REALTIVE_X");
+        names.append(AXIS_RELATIVE_Y, "AXIS_REALTIVE_Y");
         names.append(AXIS_GENERIC_1, "AXIS_GENERIC_1");
         names.append(AXIS_GENERIC_2, "AXIS_GENERIC_2");
         names.append(AXIS_GENERIC_3, "AXIS_GENERIC_3");
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index afa6c78..2d7ea2e 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -14716,6 +14716,7 @@
         destroyDrawingCache();
 
         cleanupDraw();
+        releasePointerCapture();
         mCurrentAnimation = null;
     }
 
@@ -21212,6 +21213,56 @@
         mPointerIcon = pointerIcon;
     }
 
+    /**
+     * Request capturing further mouse events.
+     *
+     * When the view captures, the mouse pointer icon will disappear and will not change its
+     * position. Further mouse events will come to the capturing view, and the mouse movements
+     * will can be detected through {@link MotionEvent#AXIS_RELATIVE_X} and
+     * {@link MotionEvent#AXIS_RELATIVE_Y}. Non-mouse events (touchscreens, or stylus) will not
+     * be affected.
+     *
+     * The capture will be released through {@link #releasePointerCapture()}, or will be lost
+     * automatically when the view or containing window disappear.
+     *
+     * @return true when succeeds.
+     * @see #releasePointerCapture()
+     * @see #hasPointerCapture()
+     */
+    public void setPointerCapture() {
+        final ViewRootImpl viewRootImpl = getViewRootImpl();
+        if (viewRootImpl != null) {
+            viewRootImpl.setPointerCapture(this);
+        }
+    }
+
+
+    /**
+     * Release the current capture of mouse events.
+     *
+     * If the view does not have the capture, it will do nothing.
+     * @see #setPointerCapture()
+     * @see #hasPointerCapture()
+     */
+    public void releasePointerCapture() {
+        final ViewRootImpl viewRootImpl = getViewRootImpl();
+        if (viewRootImpl != null) {
+            viewRootImpl.releasePointerCapture(this);
+        }
+    }
+
+    /**
+     * Checks the capture status of mouse events.
+     *
+     * @return true if the view has the capture.
+     * @see #setPointerCapture()
+     * @see #hasPointerCapture()
+     */
+    public boolean hasPointerCapture() {
+        final ViewRootImpl viewRootImpl = getViewRootImpl();
+        return (viewRootImpl != null) && viewRootImpl.hasPointerCapture(this);
+    }
+
     //
     // Properties
     //
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index faf2640..f758fef 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -39,6 +39,7 @@
 import android.graphics.drawable.Drawable;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManager.DisplayListener;
+import android.hardware.input.InputManager;
 import android.media.AudioManager;
 import android.os.Binder;
 import android.os.Build;
@@ -174,6 +175,9 @@
     View mAccessibilityFocusedHost;
     AccessibilityNodeInfo mAccessibilityFocusedVirtualView;
 
+    // The view which captures mouse input, or null when no one is capturing.
+    View mCapturingView;
+
     int mViewVisibility;
     boolean mAppVisible = true;
     // For recents to freeform transition we need to keep drawing after the app receives information
@@ -3004,6 +3008,31 @@
         }
     }
 
+    void setPointerCapture(View view) {
+        if (!mAttachInfo.mHasWindowFocus) {
+            Log.w(TAG, "Can't set capture if it's not focused.");
+            return;
+        }
+        if (mCapturingView == view) {
+            return;
+        }
+        mCapturingView = view;
+        InputManager.getInstance().setPointerIconDetached(true);
+    }
+
+    void releasePointerCapture(View view) {
+        if (mCapturingView != view || mCapturingView == null) {
+            return;
+        }
+
+        mCapturingView = null;
+        InputManager.getInstance().setPointerIconDetached(false);
+    }
+
+    boolean hasPointerCapture(View view) {
+        return view != null && mCapturingView == view;
+    }
+
     @Override
     public void requestChildFocus(View child, View focused) {
         if (DEBUG_INPUT_RESIZE) {
@@ -3081,6 +3110,10 @@
         mView = null;
         mAttachInfo.mRootView = null;
 
+        if (mCapturingView != null) {
+            releasePointerCapture(mCapturingView);
+        }
+
         mSurface.release();
 
         if (mInputQueueCallback != null && mInputQueue != null) {
@@ -3390,6 +3423,8 @@
                                 .softInputMode &=
                                     ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
                         mHasHadWindowFocus = true;
+                    } else if (mCapturingView != null) {
+                        releasePointerCapture(mCapturingView);
                     }
                 }
             } break;
@@ -4236,7 +4271,10 @@
             }
 
             mAttachInfo.mUnbufferedDispatchRequested = false;
-            boolean handled = mView.dispatchPointerEvent(event);
+            final View eventTarget =
+                    (event.isFromSource(InputDevice.SOURCE_MOUSE) && mCapturingView != null) ?
+                            mCapturingView : mView;
+            boolean handled = eventTarget.dispatchPointerEvent(event);
             if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
                 mUnbufferedInputDispatch = true;
                 if (mConsumeBatchedInputScheduled) {
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 529849e..212c6a0 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -90,6 +90,7 @@
     mLocked.lastFrameUpdatedTime = 0;
 
     mLocked.buttonState = 0;
+    mLocked.iconDetached = false;
 
     mPolicy->loadPointerIcon(&mLocked.pointerIcon);
 
@@ -184,6 +185,10 @@
 }
 
 void PointerController::setPositionLocked(float x, float y) {
+    if (mLocked.iconDetached) {
+        return;
+    }
+
     float minX, minY, maxX, maxY;
     if (getBoundsLocked(&minX, &minY, &maxX, &maxY)) {
         if (x <= minX) {
@@ -217,6 +222,10 @@
     // Remove the inactivity timeout, since we are fading now.
     removeInactivityTimeoutLocked();
 
+    if (mLocked.iconDetached) {
+        return;
+    }
+
     // Start fading.
     if (transition == TRANSITION_IMMEDIATE) {
         mLocked.pointerFadeDirection = 0;
@@ -234,6 +243,10 @@
     // Always reset the inactivity timer.
     resetInactivityTimeoutLocked();
 
+    if (mLocked.iconDetached) {
+        return;
+    }
+
     // Start unfading.
     if (transition == TRANSITION_IMMEDIATE) {
         mLocked.pointerFadeDirection = 0;
@@ -349,6 +362,22 @@
     updatePointerLocked();
 }
 
+void PointerController::detachPointerIcon(bool detached) {
+    AutoMutex _l(mLock);
+
+    if (mLocked.iconDetached == detached) {
+        return;
+    }
+
+    mLocked.iconDetached = detached;
+    if (detached) {
+        mLocked.pointerFadeDirection = -1;
+    } else {
+        mLocked.pointerFadeDirection = 1;
+    }
+    startAnimationLocked();
+}
+
 void PointerController::setDisplayViewport(int32_t width, int32_t height, int32_t orientation) {
     AutoMutex _l(mLock);
 
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 9ba37b3..c1381f3 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -111,6 +111,10 @@
     void setInactivityTimeout(InactivityTimeout inactivityTimeout);
     void reloadPointerResources();
 
+    /* Detach or attach the pointer icon status. When detached, the pointer icon disappears
+     * and the icon location does not change at all. */
+    void detachPointerIcon(bool detached);
+
 private:
     static const size_t MAX_RECYCLED_SPRITES = 12;
     static const size_t MAX_SPOTS = 12;
@@ -180,6 +184,8 @@
 
         int32_t buttonState;
 
+        bool iconDetached;
+
         Vector<Spot*> spots;
         Vector<sp<Sprite> > recycledSprites;
     } mLocked;
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 68b3817..9f74fc6 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -214,6 +214,7 @@
     private static native void nativeSetPointerIconShape(long ptr, int iconId);
     private static native void nativeReloadPointerIcons(long ptr);
     private static native void nativeSetCustomPointerIcon(long ptr, PointerIcon icon);
+    private static native void nativeSetPointerIconDetached(long ptr, boolean detached);
 
     // Input event injection constants defined in InputDispatcher.h.
     private static final int INPUT_EVENT_INJECTION_SUCCEEDED = 0;
@@ -1274,6 +1275,11 @@
         nativeSetFocusedApplication(mPtr, application);
     }
 
+    @Override
+    public void setPointerIconDetached(boolean detached) {
+        nativeSetPointerIconDetached(mPtr, detached);
+    }
+
     public void setInputDispatchMode(boolean enabled, boolean frozen) {
         nativeSetInputDispatchMode(mPtr, enabled, frozen);
     }
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 4c8474a..a5237ca 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -207,6 +207,7 @@
     void setPointerIconShape(int32_t iconId);
     void reloadPointerIcons();
     void setCustomPointerIcon(const SpriteIcon& icon);
+    void setPointerIconDetached(bool detached);
 
     /* --- InputReaderPolicyInterface implementation --- */
 
@@ -711,6 +712,14 @@
     mInputManager->getDispatcher()->setFocusedApplication(applicationHandle);
 }
 
+void NativeInputManager::setPointerIconDetached(bool detached) {
+    AutoMutex _l(mLock);
+    sp<PointerController> controller = mLocked.pointerController.promote();
+    if (controller != NULL) {
+        controller->detachPointerIcon(detached);
+    }
+}
+
 void NativeInputManager::setInputDispatchMode(bool enabled, bool frozen) {
     mInputManager->getDispatcher()->setInputDispatchMode(enabled, frozen);
 }
@@ -1317,6 +1326,12 @@
     im->setFocusedApplication(env, applicationHandleObj);
 }
 
+static void nativeSetPointerIconDetached(JNIEnv* env, jclass /* clazz */, jlong ptr,
+        jboolean detached) {
+    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    im->setPointerIconDetached(detached);
+}
+
 static void nativeSetInputDispatchMode(JNIEnv* /* env */,
         jclass /* clazz */, jlong ptr, jboolean enabled, jboolean frozen) {
     NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
@@ -1497,6 +1512,8 @@
             (void*) nativeSetInputWindows },
     { "nativeSetFocusedApplication", "(JLcom/android/server/input/InputApplicationHandle;)V",
             (void*) nativeSetFocusedApplication },
+    { "nativeSetPointerIconDetached", "(JZ)V",
+            (void*) nativeSetPointerIconDetached },
     { "nativeSetInputDispatchMode", "(JZZ)V",
             (void*) nativeSetInputDispatchMode },
     { "nativeSetSystemUiVisibility", "(JI)V",