Snap for 5702388 from a0967d21e645569b844e2ab5b22e843689525ce9 to qt-aml-release

Change-Id: I311623f284c4b01793acaa98c5190dca6de959bf
diff --git a/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java b/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java
index eb0c5b9..057b48b 100644
--- a/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ b/go/quickstep/src/com/android/quickstep/FallbackActivityControllerHelper.java
@@ -113,4 +113,7 @@
     public int getContainerType() {
         return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER;
     }
+
+    @Override
+    public void onLaunchTaskSuccess(RecentsActivity activity) { }
 }
diff --git a/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java b/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java
index d595007..b0d9cda 100644
--- a/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/go/quickstep/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -104,4 +104,9 @@
         return launcher != null ? launcher.getStateManager().getState().containerType
                 : LauncherLogProto.ContainerType.APP;
     }
+
+    @Override
+    public void onLaunchTaskSuccess(Launcher launcher) {
+        launcher.getStateManager().moveToRestState();
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index d14de70..5ee08c1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -81,7 +81,8 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return super.getVisibleElements(launcher) & ~RECENTS_CLEAR_ALL_BUTTON;
+        return super.getVisibleElements(launcher)
+                & ~RECENTS_CLEAR_ALL_BUTTON & ~VERTICAL_SWIPE_INDICATOR;
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
index cdc271f..6c9f46f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/QuickSwitchState.java
@@ -57,6 +57,8 @@
                 if (!success) {
                     launcher.getStateManager().goToState(OVERVIEW);
                     tasktolaunch.notifyTaskLaunchFailed(TAG);
+                } else {
+                    launcher.getStateManager().moveToRestState();
                 }
             }, new Handler(Looper.getMainLooper()));
         } else {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
index 4ae6d87..c43155b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackActivityControllerHelper.java
@@ -230,4 +230,9 @@
         // TODO: probably go back to overview instead.
         activity.<RecentsView>getOverviewPanel().startHome();
     }
+
+    @Override
+    public void onLaunchTaskSuccess(RecentsActivity activity) {
+        activity.onTaskLaunched();
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
index 07c0496..b2a71a4 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityControllerHelper.java
@@ -488,4 +488,9 @@
     public void onLaunchTaskFailed(Launcher launcher) {
         launcher.getStateManager().goToState(OVERVIEW);
     }
+
+    @Override
+    public void onLaunchTaskSuccess(Launcher launcher) {
+        launcher.getStateManager().moveToRestState();
+    }
 }
\ No newline at end of file
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
index 32a9261..fc29a56 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/RecentsActivity.java
@@ -177,4 +177,8 @@
         super.onStart();
         mFallbackRecentsView.resetTaskVisuals();
     }
+
+    public void onTaskLaunched() {
+        mFallbackRecentsView.resetTaskVisuals();
+    }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
index e9aa6d0..f1d1141 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -1259,6 +1259,8 @@
                                 mActivityControlHelper.onLaunchTaskFailed(mActivity);
                                 nextTask.notifyTaskLaunchFailed(TAG);
                                 updateSysUiFlags(1 /* windowProgress == overview */);
+                            } else {
+                                mActivityControlHelper.onLaunchTaskSuccess(mActivity);
                             }
                         }, mMainThreadHandler);
                         doLogGesture(NEW_TASK);
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 71259fd..b0968f9 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -38,7 +38,7 @@
     <dimen name="motion_pause_detector_speed_very_slow">0.0285dp</dimen>
     <dimen name="motion_pause_detector_speed_slow">0.15dp</dimen>
     <dimen name="motion_pause_detector_speed_somewhat_fast">0.285dp</dimen>
-    <dimen name="motion_pause_detector_speed_fast">0.5dp</dimen>
+    <dimen name="motion_pause_detector_speed_fast">1.4dp</dimen>
     <dimen name="motion_pause_detector_min_displacement_from_app">36dp</dimen>
 
     <!-- Launcher app transition -->
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
index f5ba372..fee1820 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/StatusBarTouchController.java
@@ -17,22 +17,17 @@
 
 import static android.view.MotionEvent.ACTION_DOWN;
 import static android.view.MotionEvent.ACTION_MOVE;
-import static android.view.MotionEvent.ACTION_UP;
-import static android.view.MotionEvent.ACTION_CANCEL;
 
-import android.graphics.PointF;
 import android.os.RemoteException;
 import android.util.Log;
-import android.util.SparseArray;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
-import android.view.Window;
-import android.view.WindowManager;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.touch.TouchEventTranslator;
 import com.android.launcher3.util.TouchController;
 import com.android.quickstep.RecentsModel;
 import com.android.systemui.shared.recents.ISystemUiProxy;
@@ -41,29 +36,18 @@
 
 /**
  * TouchController for handling touch events that get sent to the StatusBar. Once the
- * Once the event delta mDownY passes the touch slop, the events start getting forwarded.
+ * Once the event delta y passes the touch slop, the events start getting forwarded.
  * All events are offset by initial Y value of the pointer.
  */
 public class StatusBarTouchController implements TouchController {
 
     private static final String TAG = "StatusBarController";
 
-    /**
-     * Window flag: Enable touches to slide out of a window into neighboring
-     * windows in mid-gesture instead of being captured for the duration of
-     * the gesture.
-     *
-     * This flag changes the behavior of touch focus for this window only.
-     * Touches can slide out of the window but they cannot necessarily slide
-     * back in (unless the other window with touch focus permits it).
-     */
-    private static final int FLAG_SLIPPERY = 0x20000000;
-
     protected final Launcher mLauncher;
+    protected final TouchEventTranslator mTranslator;
     private final float mTouchSlop;
     private ISystemUiProxy mSysUiProxy;
     private int mLastAction;
-    private final SparseArray<PointF> mDownEvents;
 
     /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/
     private boolean mCanIntercept;
@@ -72,7 +56,7 @@
         mLauncher = l;
         // Guard against TAPs by increasing the touch slop.
         mTouchSlop = 2 * ViewConfiguration.get(l).getScaledTouchSlop();
-        mDownEvents = new SparseArray<>();
+        mTranslator = new TouchEventTranslator((MotionEvent ev)-> dispatchTouchEvent(ev));
     }
 
     @Override
@@ -80,6 +64,7 @@
         writer.println(prefix + "mCanIntercept:" + mCanIntercept);
         writer.println(prefix + "mLastAction:" + MotionEvent.actionToString(mLastAction));
         writer.println(prefix + "mSysUiProxy available:" + (mSysUiProxy != null));
+
     }
 
     private void dispatchTouchEvent(MotionEvent ev) {
@@ -96,31 +81,26 @@
     @Override
     public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
         int action = ev.getActionMasked();
-        int idx = ev.getActionIndex();
-        int pid = ev.getPointerId(idx);
         if (action == ACTION_DOWN) {
             mCanIntercept = canInterceptTouch(ev);
             if (!mCanIntercept) {
                 return false;
             }
-            mDownEvents.put(pid, new PointF(ev.getX(), ev.getY()));
+            mTranslator.reset();
+            mTranslator.setDownParameters(0, ev);
         } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
-           // Check!! should only set it only when threshold is not entered.
-           mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx)));
+            // Check!! should only set it only when threshold is not entered.
+            mTranslator.setDownParameters(ev.getActionIndex(), ev);
         }
         if (!mCanIntercept) {
             return false;
         }
         if (action == ACTION_MOVE) {
-            float dy = ev.getY(idx) - mDownEvents.get(pid).y;
-            float dx = ev.getX(idx) - mDownEvents.get(pid).x;
-            // Currently input dispatcher will not do touch transfer if there are more than
-            // one touch pointer. Hence, even if slope passed, only set the slippery flag
-            // when there is single touch event. (context: InputDispatcher.cpp line 1445)
-            if (dy > mTouchSlop && dy > Math.abs(dx) && ev.getPointerCount() == 1) {
-                ev.setAction(ACTION_DOWN);
-                dispatchTouchEvent(ev);
-                setWindowSlippery(true);
+            float dy = ev.getY() - mTranslator.getDownY();
+            float dx = ev.getX() - mTranslator.getDownX();
+            if (dy > mTouchSlop && dy > Math.abs(dx)) {
+                mTranslator.dispatchDownEvents(ev);
+                mTranslator.processMotionEvent(ev);
                 return true;
             }
             if (Math.abs(dx) > mTouchSlop) {
@@ -130,27 +110,13 @@
         return false;
     }
 
+
     @Override
     public final boolean onControllerTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == ACTION_UP || ev.getAction() == ACTION_CANCEL) {
-            dispatchTouchEvent(ev);
-            setWindowSlippery(false);
-            return true;
-        }
+        mTranslator.processMotionEvent(ev);
         return true;
     }
 
-    private void setWindowSlippery(boolean enable) {
-        Window w = mLauncher.getWindow();
-        WindowManager.LayoutParams wlp = w.getAttributes();
-        if (enable) {
-            wlp.flags |= FLAG_SLIPPERY;
-        } else {
-            wlp.flags &= ~FLAG_SLIPPERY;
-        }
-        w.setAttributes(wlp);
-    }
-
     private boolean canInterceptTouch(MotionEvent ev) {
         if (!mLauncher.isInState(LauncherState.NORMAL) ||
                 AbstractFloatingView.getTopOpenViewWithType(mLauncher,
@@ -166,4 +132,4 @@
         mSysUiProxy = RecentsModel.INSTANCE.get(mLauncher).getSystemUiProxy();
         return mSysUiProxy != null;
     }
-}
\ No newline at end of file
+}
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 8675c3e..cd2c9cb 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -95,6 +95,8 @@
 
     void onLaunchTaskFailed(T activity);
 
+    void onLaunchTaskSuccess(T activity);
+
     interface ActivityInitListener {
 
         void register();
diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
index b6ddb5f..63c8023 100644
--- a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
+++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
@@ -29,6 +30,7 @@
 import android.graphics.Path;
 import android.graphics.Path.Direction;
 import android.graphics.Path.Op;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.animation.Interpolator;
 
@@ -36,6 +38,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.uioverrides.states.OverviewState;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
 import com.android.quickstep.SysUINavigationMode;
@@ -145,14 +148,22 @@
             mRemainingScreenPathValid = false;
             mShiftRange = mLauncher.getAllAppsController().getShiftRange();
 
-            mMidProgress = OVERVIEW.getVerticalProgress(mLauncher);
-            mMidAlpha = mMidProgress >= 1 ? 0
-                    : Themes.getAttrInteger(getContext(), R.attr.allAppsInterimScrimAlpha);
+            if ((OVERVIEW.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) == 0) {
+                mMidProgress = 1;
+                mMidAlpha = 0;
+            } else {
+                mMidAlpha = Themes.getAttrInteger(getContext(), R.attr.allAppsInterimScrimAlpha);
+                Rect hotseatPadding = dp.getHotseatLayoutPadding();
+                int hotseatSize = dp.hotseatBarSizePx + dp.getInsets().bottom
+                        - hotseatPadding.bottom - hotseatPadding.top;
+                float arrowTop = Math.min(hotseatSize, OverviewState.getDefaultSwipeHeight(dp));
+                mMidProgress =  1 - (arrowTop / mShiftRange);
 
+            }
             mTopOffset = dp.getInsets().top - mShelfOffset;
             mShelfTopAtThreshold = mShiftRange * SCRIM_CATCHUP_THRESHOLD + mTopOffset;
-            updateColors();
         }
+        updateColors();
         updateDragHandleAlpha();
         invalidate();
     }
diff --git a/src/com/android/launcher3/touch/TouchEventTranslator.java b/src/com/android/launcher3/touch/TouchEventTranslator.java
new file mode 100644
index 0000000..3fcda90
--- /dev/null
+++ b/src/com/android/launcher3/touch/TouchEventTranslator.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2018 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.launcher3.touch;
+
+import android.graphics.PointF;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+
+import java.util.function.Consumer;
+
+/**
+ * To minimize the size of the MotionEvent, historic events are not copied and passed via the
+ * listener.
+ */
+public class TouchEventTranslator {
+
+    private static final String TAG = "TouchEventTranslator";
+    private static final boolean DEBUG = false;
+
+    private class DownState {
+        long timeStamp;
+        float downX;
+        float downY;
+        public DownState(long timeStamp, float downX, float downY) {
+            this.timeStamp = timeStamp;
+            this.downX = downX;
+            this.downY = downY;
+        }
+    };
+    private final DownState ZERO = new DownState(0, 0f, 0f);
+
+    private final Consumer<MotionEvent> mListener;
+
+    private final SparseArray<DownState> mDownEvents;
+    private final SparseArray<PointF> mFingers;
+
+    private final SparseArray<Pair<PointerProperties[], PointerCoords[]>> mCache;
+
+    public TouchEventTranslator(Consumer<MotionEvent> listener) {
+        mDownEvents = new SparseArray<>();
+        mFingers = new SparseArray<>();
+        mCache = new SparseArray<>();
+
+        mListener = listener;
+    }
+
+    public void reset() {
+        mDownEvents.clear();
+        mFingers.clear();
+    }
+
+    public float getDownX() {
+        return mDownEvents.get(0).downX;
+    }
+
+    public float getDownY() {
+        return mDownEvents.get(0).downY;
+    }
+
+    public void setDownParameters(int idx, MotionEvent e) {
+        DownState ev = new DownState(e.getEventTime(), e.getX(idx), e.getY(idx));
+        mDownEvents.append(idx, ev);
+    }
+
+    public void dispatchDownEvents(MotionEvent ev) {
+        for(int i = 0; i < ev.getPointerCount() && i < mDownEvents.size(); i++) {
+            int pid = ev.getPointerId(i);
+            put(pid, i, ev.getX(i), 0, mDownEvents.get(i).timeStamp, ev);
+        }
+    }
+
+    public void processMotionEvent(MotionEvent ev) {
+        if (DEBUG) {
+            printSamples(TAG + " processMotionEvent", ev);
+        }
+        int index = ev.getActionIndex();
+        float x = ev.getX(index);
+        float y = ev.getY(index) - mDownEvents.get(index, ZERO).downY;
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_POINTER_DOWN:
+                int pid = ev.getPointerId(index);
+                if(mFingers.get(pid, null) != null) {
+                    for(int i=0; i < ev.getPointerCount(); i++) {
+                        pid = ev.getPointerId(i);
+                        position(pid, x, y);
+                    }
+                    generateEvent(ev.getAction(), ev);
+                } else {
+                    put(pid, index, x, y, ev);
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                for(int i=0; i < ev.getPointerCount(); i++) {
+                    pid = ev.getPointerId(i);
+                    position(pid, x, y);
+                }
+                generateEvent(ev.getAction(), ev);
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+            case MotionEvent.ACTION_UP:
+                pid = ev.getPointerId(index);
+                lift(pid, index, x, y, ev);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                cancel(ev);
+                break;
+            default:
+                Log.v(TAG, "Didn't process ");
+                printSamples(TAG, ev);
+
+        }
+    }
+
+    private TouchEventTranslator put(int id, int index, float x, float y, MotionEvent ev) {
+        return put(id, index, x, y, ev.getEventTime(), ev);
+    }
+
+    private TouchEventTranslator put(int id, int index, float x, float y, long ms, MotionEvent ev) {
+        checkFingerExistence(id, false);
+        boolean isInitialDown = (mFingers.size() == 0);
+
+        mFingers.put(id, new PointF(x, y));
+        int n = mFingers.size();
+
+        if (mCache.get(n) == null) {
+            PointerProperties[] properties = new PointerProperties[n];
+            PointerCoords[] coords = new PointerCoords[n];
+            for (int i = 0; i < n; i++) {
+                properties[i] = new PointerProperties();
+                coords[i] = new PointerCoords();
+            }
+            mCache.put(n, new Pair(properties, coords));
+        }
+
+        int action;
+        if (isInitialDown) {
+            action = MotionEvent.ACTION_DOWN;
+        } else {
+            action = MotionEvent.ACTION_POINTER_DOWN;
+            // Set the id of the changed pointer.
+            action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        }
+        generateEvent(action, ms, ev);
+        return this;
+    }
+
+    public TouchEventTranslator position(int id, float x, float y) {
+        checkFingerExistence(id, true);
+        mFingers.get(id).set(x, y);
+        return this;
+    }
+
+    private TouchEventTranslator lift(int id, int index, MotionEvent ev) {
+        checkFingerExistence(id, true);
+        boolean isFinalUp = (mFingers.size() == 1);
+        int action;
+        if (isFinalUp) {
+            action = MotionEvent.ACTION_UP;
+        } else {
+            action = MotionEvent.ACTION_POINTER_UP;
+            // Set the id of the changed pointer.
+            action |= index << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        }
+        generateEvent(action, ev);
+        mFingers.remove(id);
+        return this;
+    }
+
+    private TouchEventTranslator lift(int id, int index, float x, float y, MotionEvent ev) {
+        checkFingerExistence(id, true);
+        mFingers.get(id).set(x, y);
+        return lift(id, index, ev);
+    }
+
+    public TouchEventTranslator cancel(MotionEvent ev) {
+        generateEvent(MotionEvent.ACTION_CANCEL, ev);
+        mFingers.clear();
+        return this;
+    }
+
+    private void checkFingerExistence(int id, boolean shouldExist) {
+        if (shouldExist != (mFingers.get(id, null) != null)) {
+            throw new IllegalArgumentException(
+                    shouldExist ? "Finger does not exist" : "Finger already exists");
+        }
+    }
+
+
+    /**
+     * Used to debug MotionEvents being sent/received.
+     */
+    public void printSamples(String msg, MotionEvent ev) {
+        System.out.printf("%s %s", msg, MotionEvent.actionToString(ev.getActionMasked()));
+        final int pointerCount = ev.getPointerCount();
+        System.out.printf("#%d/%d", ev.getActionIndex(), pointerCount);
+        System.out.printf(" t=%d:", ev.getEventTime());
+        for (int p = 0; p < pointerCount; p++) {
+            System.out.printf("  id=%d: (%f,%f)",
+                    ev.getPointerId(p), ev.getX(p), ev.getY(p));
+        }
+        System.out.println();
+    }
+
+    private void generateEvent(int action, MotionEvent ev) {
+        generateEvent(action, ev.getEventTime(), ev);
+    }
+
+    private void generateEvent(int action, long ms, MotionEvent ev) {
+        Pair<PointerProperties[], PointerCoords[]> state = getFingerState();
+        MotionEvent event = MotionEvent.obtain(
+                mDownEvents.get(0).timeStamp,
+                ms,
+                action,
+                state.first.length,
+                state.first,
+                state.second,
+                ev.getMetaState(),
+                ev.getButtonState() /* buttonState */,
+                ev.getXPrecision() /* xPrecision */,
+                ev.getYPrecision() /* yPrecision */,
+                ev.getDeviceId(),
+                ev.getEdgeFlags(),
+                ev.getSource(),
+                ev.getFlags() /* flags */);
+        if (DEBUG) {
+            printSamples(TAG + " generateEvent", event);
+        }
+        if (event.getPointerId(event.getActionIndex()) < 0) {
+            printSamples(TAG + "generateEvent", event);
+            throw new IllegalStateException(event.getActionIndex() + " not found in MotionEvent");
+        }
+        mListener.accept(event);
+        event.recycle();
+    }
+
+    /**
+     * Returns the description of the fingers' state expected by MotionEvent.
+     */
+    private Pair<PointerProperties[], PointerCoords[]> getFingerState() {
+        int nFingers = mFingers.size();
+
+        Pair<PointerProperties[], PointerCoords[]> result = mCache.get(nFingers);
+        PointerProperties[] properties = result.first;
+        PointerCoords[] coordinates = result.second;
+
+        int index = 0;
+        for (int i = 0; i < mFingers.size(); i++) {
+            int id = mFingers.keyAt(i);
+            PointF location = mFingers.get(id);
+
+            PointerProperties property = properties[i];
+            property.id = id;
+            property.toolType = MotionEvent.TOOL_TYPE_FINGER;
+            properties[index] = property;
+
+            PointerCoords coordinate = coordinates[i];
+            coordinate.x = location.x;
+            coordinate.y = location.y;
+            coordinate.pressure = 1.0f;
+            coordinates[index] = coordinate;
+
+            index++;
+        }
+        return mCache.get(nFingers);
+    }
+}