Initial changes for handling touch events.

When the touch passes a threashold, we take a snapshot and start the launcher
activity. The launcher displays the snapshot on top of its UI.
As we get further touch events, we move this snapshot and the hotseat in reponse.

Change-Id: I4623676227000afd52805a414a4de499081feced
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index 466470f..521551c 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -19,28 +19,33 @@
     android:theme="@style/HomeScreenElementTheme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:paddingTop="20dp"
+    android:paddingBottom="20dp"
+    android:clipToPadding="false"
     android:layout_gravity="center_horizontal|bottom"
     android:gravity="top">
 
     <LinearLayout
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
+        android:paddingStart="40dp"
+        android:paddingEnd="40dp"
         android:orientation="horizontal">
 
-        <View
-            android:layout_width="300dp"
+        <com.android.quickstep.SimpleTaskView
+            android:layout_width="wrap_content"
             android:layout_height="match_parent"
             android:background="#44FF0000"
             android:layout_marginEnd="10dp"/>
 
-        <View
-            android:layout_width="300dp"
+        <com.android.quickstep.SimpleTaskView
+            android:layout_width="wrap_content"
             android:layout_height="match_parent"
             android:background="#4400FF00"
             android:layout_marginEnd="10dp"/>
 
-        <View
-            android:layout_width="300dp"
+        <com.android.quickstep.SimpleTaskView
+            android:layout_width="wrap_content"
             android:layout_height="match_parent"
             android:background="#440000FF" />
     </LinearLayout>
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
new file mode 100644
index 0000000..caeef50
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2017 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.quickstep;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.RectEvaluator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.UserHandle;
+import android.support.annotation.BinderThread;
+import android.support.annotation.UiThread;
+import android.util.FloatProperty;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.states.InternalStateHandler;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+@TargetApi(Build.VERSION_CODES.O)
+public class NavBarSwipeInteractionHandler extends InternalStateHandler implements FrameCallback {
+
+    private static FloatProperty<NavBarSwipeInteractionHandler> SHIFT =
+            new FloatProperty<NavBarSwipeInteractionHandler>("currentShift") {
+        @Override
+        public void setValue(NavBarSwipeInteractionHandler handler, float v) {
+            handler.setShift(v);
+        }
+
+        @Override
+        public Float get(NavBarSwipeInteractionHandler handler) {
+            return handler.mCurrentShift;
+        }
+    };
+
+    // The following constants need to be scaled based on density. The scaled versions will be
+    // assigned to the corresponding member variables below.
+    private static final int FLING_THRESHOLD_VELOCITY = 500;
+    private static final int MIN_FLING_VELOCITY = 250;
+
+    private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
+
+    private final Rect mSourceRect = new Rect();
+    private final Rect mTargetRect = new Rect();
+    private final Rect mCurrentRect = new Rect();
+    private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
+
+    private final Bitmap mTaskSnapshot;
+    private final RunningTaskInfo mTaskInfo;
+
+    private Launcher mLauncher;
+    private Choreographer mChoreographer;
+    private SnapshotDragView mDragView;
+    private RecentsView mRecentsView;
+    private Hotseat mHotseat;
+
+    private float mStartDelta;
+    private float mLastDelta;
+
+    // Shift in the range of [0, 1].
+    // 0 => preview snapShot is completely visible, and hotseat is completely translated down
+    // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
+    // visible.
+    private float mCurrentShift;
+
+    // These are updated on the binder thread, and eventually picked up on doFrame
+    private float mCurrentDisplacement;
+    private boolean mTouchEnded = false;
+    private float mEndVelocity;
+
+    NavBarSwipeInteractionHandler(Bitmap taskSnapShot, RunningTaskInfo taskInfo) {
+        mTaskSnapshot = taskSnapShot;
+        mTaskInfo = taskInfo;
+    }
+
+    @Override
+    public void onLauncherResume() {
+        mStartDelta = mCurrentDisplacement;
+        mLastDelta = mStartDelta;
+        mChoreographer = Choreographer.getInstance();
+
+        scheduleNextFrame();
+    }
+
+    @Override
+    public void onNewIntent(Launcher launcher) {
+        mLauncher = launcher;
+
+        // Go immediately
+        launcher.getStateManager().goToState(LauncherState.OVERVIEW, false);
+
+        // Optimization
+        launcher.getAppsView().setVisibility(View.GONE);
+
+        mDragView = new SnapshotDragView(launcher, mTaskSnapshot);
+        launcher.getDragLayer().addView(mDragView);
+        mDragView.setPivotX(0);
+        mDragView.setPivotY(0);
+        mRecentsView = launcher.getOverviewPanel();
+        mRecentsView.scrollTo(0, 0);
+        mHotseat = launcher.getHotseat();
+    }
+
+    @BinderThread
+    public void updateDisplacement(float displacement) {
+        mCurrentDisplacement = displacement;
+    }
+
+    @BinderThread
+    public void endTouch(float endVelocity) {
+        mTouchEnded = true;
+        mEndVelocity = endVelocity;
+    }
+
+    @UiThread
+    private void scheduleNextFrame() {
+        if (!mTouchEnded) {
+            mChoreographer.postFrameCallback(this);
+        } else {
+            animateToFinalShift();
+        }
+    }
+
+    @Override
+    public void doFrame(long l) {
+        mLastDelta = mCurrentDisplacement;
+
+        float translation = Utilities.boundToRange(mStartDelta - mLastDelta, 0,
+                mHotseat.getHeight());
+        int hotseatHeight = mHotseat.getHeight();
+        float shift = hotseatHeight == 0 ? 0 : translation / hotseatHeight;
+        setShift(shift);
+        scheduleNextFrame();
+    }
+
+    @UiThread
+    private void setShift(float shift) {
+        if (mTargetRect.isEmpty()) {
+            DragLayer dl = mLauncher.getDragLayer();
+
+            // Init target rect.
+            View targetView = ((ViewGroup) mRecentsView.getChildAt(0)).getChildAt(0);
+            dl.getViewRectRelativeToSelf(targetView, mTargetRect);
+            mSourceRect.set(0, 0, dl.getWidth(), dl.getHeight());
+        }
+
+        mCurrentShift = shift;
+        int hotseatHeight = mHotseat.getHeight();
+        mHotseat.setTranslationY((1 - shift) * hotseatHeight);
+
+        mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
+
+        mDragView.setTranslationX(mCurrentRect.left);
+        mDragView.setTranslationY(mCurrentRect.top);
+        mDragView.setScaleX((float) mCurrentRect.width() / mSourceRect.width());
+        mDragView.setScaleY((float) mCurrentRect.width() / mSourceRect.width());
+    }
+
+    @UiThread
+    private void animateToFinalShift() {
+        float flingThreshold = Utilities.pxFromDp(FLING_THRESHOLD_VELOCITY,
+                    mLauncher.getResources().getDisplayMetrics());
+        boolean isFling = Math.abs(mEndVelocity) > flingThreshold;
+
+        long duration = 200;
+        final float endShift;
+        if (!isFling) {
+            endShift = mCurrentShift >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
+        } else {
+            endShift = mEndVelocity < 0 ? 1 : 0;
+            float minFlingVelocity = Utilities.pxFromDp(MIN_FLING_VELOCITY,
+                    mLauncher.getResources().getDisplayMetrics());
+            if (Math.abs(mEndVelocity) > minFlingVelocity) {
+                float distanceToTravel = (endShift - mCurrentShift) * mHotseat.getHeight();
+
+                // we want the page's snap velocity to approximately match the velocity at
+                // which the user flings, so we scale the duration by a value near to the
+                // derivative of the scroll interpolator at zero, ie. 5. We use 4 to make
+                // it a little slower.
+                duration = 4 * Math.round(1000 * Math.abs(distanceToTravel / mEndVelocity));
+            }
+        }
+
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHIFT, endShift)
+                .setDuration(duration);
+        anim.setInterpolator(Interpolators.SCROLL);
+        anim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                if (Float.compare(mCurrentShift, 0) == 0) {
+                    resumeLastTask();
+                } else {
+                    mDragView.close(false);
+                }
+            }
+        });
+        anim.start();
+    }
+
+    @UiThread
+    private void resumeLastTask() {
+        // TODO: These should be done as part of ActivityOptions#OnAnimationStarted
+        mHotseat.setTranslationY(0);
+        mLauncher.setOnResumeCallback(() -> mDragView.close(false));
+
+        // TODO: Task key should be received from Recents model
+        TaskKey taskKey = new TaskKey(mTaskInfo.id, 0, null, UserHandle.myUserId(), 0);
+        ActivityManagerWrapper.getInstance()
+                .startActivityFromRecentsAsync(taskKey, null, null, null);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index d85de8f..d7559da 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -30,6 +30,7 @@
  * A placeholder view for recents
  */
 public class RecentsView extends HorizontalScrollView implements Insettable {
+
     public RecentsView(Context context) {
         this(context, null);
     }
diff --git a/quickstep/src/com/android/quickstep/SimpleTaskView.java b/quickstep/src/com/android/quickstep/SimpleTaskView.java
new file mode 100644
index 0000000..8425fa3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SimpleTaskView.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 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.quickstep;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * A simple view which keeps its size proportional to the display size
+ */
+public class SimpleTaskView extends View {
+
+    private static final Point sTempPoint = new Point();
+
+    public SimpleTaskView(Context context) {
+        super(context);
+    }
+
+    public SimpleTaskView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SimpleTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        getContext().getSystemService(WindowManager.class)
+                .getDefaultDisplay().getRealSize(sTempPoint);
+
+        int width = (int) ((float) height * sTempPoint.x / sTempPoint.y);
+        setMeasuredDimension(width, height);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SnapshotDragView.java b/quickstep/src/com/android/quickstep/SnapshotDragView.java
new file mode 100644
index 0000000..791fe9f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SnapshotDragView.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 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.quickstep;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+
+/**
+ * Floating view which shows the task snapshot allowing it to be dragged and placed.
+ */
+public class SnapshotDragView extends AbstractFloatingView implements Insettable {
+
+    private final Launcher mLauncher;
+    private final Bitmap mSnapshot;
+
+    public SnapshotDragView(Launcher launcher, Bitmap snapshot) {
+        super(launcher, null);
+        mLauncher = launcher;
+        mSnapshot = snapshot;
+        setWillNotDraw(false);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mSnapshot != null) {
+            setMeasuredDimension(mSnapshot.getWidth(), mSnapshot.getHeight());
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mSnapshot != null) {
+            canvas.drawBitmap(mSnapshot, 0, 0, null);
+        }
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        // We dont suupport animate.
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // We should probably log the weather
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_QUICKSTEP_PREVIEW) != 0;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 091ab54..dcfa53b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -15,17 +15,201 @@
  */
 package com.android.quickstep;
 
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
+import static com.android.launcher3.states.InternalStateHandler.EXTRA_STATE_HANDLER;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityOptions;
 import android.app.Service;
+import android.content.ComponentName;
 import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
 import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
 
 /**
  * Service connected by system-UI for handling touch interaction.
  */
 public class TouchInteractionService extends Service {
 
+    private static final String TAG = "TouchInteractionService";
+
+    private final IBinder mMyBinder = new IOverviewProxy.Stub() {
+
+        @Override
+        public void onMotionEvent(MotionEvent ev) {
+            handleMotionEvent(ev);
+        }
+
+        @Override
+        public void onBind(ISystemUiProxy iSystemUiProxy) throws RemoteException {
+            mISystemUiProxy = iSystemUiProxy;
+        }
+    };
+
+    private ActivityManagerWrapper mAM;
+    private RunningTaskInfo mRunningTask;
+    private Intent mHomeIntent;
+    private ComponentName mLauncher;
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private int mActivePointerId = INVALID_POINTER_ID;
+    private VelocityTracker mVelocityTracker;
+    private int mTouchSlop;
+    private NavBarSwipeInteractionHandler mInteractionHandler;
+
+    private ISystemUiProxy mISystemUiProxy;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mAM = ActivityManagerWrapper.getInstance();
+
+        mHomeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(getPackageName())
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        ResolveInfo info = getPackageManager().resolveActivity(mHomeIntent, 0);
+        mLauncher = new ComponentName(getPackageName(), info.activityInfo.name);
+        mHomeIntent.setComponent(mLauncher);
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
-        return null;
+        Log.d(TAG, "Touch service connected");
+        return mMyBinder;
+    }
+
+    private void handleMotionEvent(MotionEvent ev) {
+        if (ev.getActionMasked() != MotionEvent.ACTION_DOWN && mVelocityTracker == null) {
+            return;
+        }
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                mActivePointerId = ev.getPointerId(0);
+                mDownPos.set(ev.getX(), ev.getY());
+                mLastPos.set(mDownPos);
+                mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+
+                mRunningTask = mAM.getRunningTask();
+                if (mRunningTask == null || mRunningTask.topActivity.equals(mLauncher)) {
+                    // TODO: We could drive all-apps in this case. For now just ignore swipe.
+                    break;
+                }
+
+                if (mVelocityTracker == null) {
+                    mVelocityTracker = VelocityTracker.obtain();
+                } else {
+                    mVelocityTracker.clear();
+                }
+                mVelocityTracker.addMovement(ev);
+                if (mInteractionHandler != null) {
+                    mInteractionHandler.endTouch(0);
+                    mInteractionHandler = null;
+                }
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                int ptrIdx = ev.getActionIndex();
+                int ptrId = ev.getPointerId(ptrIdx);
+                if (ptrId == mActivePointerId) {
+                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                    mDownPos.set(
+                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                    mActivePointerId = ev.getPointerId(newPointerIdx);
+                    mVelocityTracker.clear();
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == INVALID_POINTER_ID) {
+                    break;
+                }
+                mVelocityTracker.addMovement(ev);
+                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                float displacement = ev.getY(pointerIndex) - mDownPos.y;
+                if (mInteractionHandler == null) {
+                    if (Math.abs(displacement) >= mTouchSlop) {
+                        startTouchTracking();
+                    }
+                } else {
+                    // Move
+                    mInteractionHandler.updateDisplacement(displacement);
+                }
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL:
+                // TODO: Should be different than ACTION_UP
+            case MotionEvent.ACTION_UP: {
+
+                endInteraction();
+                break;
+            }
+        }
+    }
+
+    private void startTouchTracking() {
+        mInteractionHandler = new NavBarSwipeInteractionHandler(getCurrentTaskSnapshot(), mRunningTask);
+
+        Bundle extras = new Bundle();
+        extras.putBinder(EXTRA_STATE_HANDLER, mInteractionHandler);
+        Intent homeIntent = new Intent(mHomeIntent).putExtras(extras);
+
+        // TODO: Call ActivityManager#startRecentsActivity instead, so that the system knows that
+        // recents was started and not Home.
+        startActivity(homeIntent,
+                ActivityOptions.makeCustomAnimation(this, 0, 0).toBundle());
+    }
+
+    private void endInteraction() {
+        if (mInteractionHandler != null) {
+            mVelocityTracker.computeCurrentVelocity(1000,
+                    ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
+
+            mInteractionHandler.endTouch(mVelocityTracker.getXVelocity(mActivePointerId));
+            mInteractionHandler = null;
+        }
+        mVelocityTracker.recycle();
+        mVelocityTracker = null;
+    }
+
+    private Bitmap getCurrentTaskSnapshot() {
+        if (mISystemUiProxy == null) {
+            Log.e(TAG, "Never received systemUIProxy");
+            return null;
+        }
+        Display display = getSystemService(WindowManager.class).getDefaultDisplay();
+        Point size = new Point();
+        display.getRealSize(size);
+
+        // TODO: We are using some hardcoded layers for now, to best approximate the activity layers
+        try {
+            return mISystemUiProxy.screenshot(new Rect(), size.x, size.y, 0, 100000, false,
+                    display.getRotation());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error capturing snapshot", e);
+            return null;
+        }
     }
 }
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 62e0fb1..26024e5 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.support.annotation.IntDef;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.LinearLayout;
@@ -42,7 +41,8 @@
             TYPE_ACTION_POPUP,
             TYPE_WIDGETS_BOTTOM_SHEET,
             TYPE_WIDGET_RESIZE_FRAME,
-            TYPE_WIDGETS_FULL_SHEET
+            TYPE_WIDGETS_FULL_SHEET,
+            TYPE_QUICKSTEP_PREVIEW
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -51,9 +51,11 @@
     public static final int TYPE_WIDGETS_BOTTOM_SHEET = 1 << 2;
     public static final int TYPE_WIDGET_RESIZE_FRAME = 1 << 3;
     public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4;
+    public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 5;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
-            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET;
+            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
+            | TYPE_QUICKSTEP_PREVIEW;
 
     protected boolean mIsOpen;
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index baed44d..b1b3452 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -63,6 +63,7 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
@@ -123,6 +124,7 @@
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.states.AllAppsState;
+import com.android.launcher3.states.InternalStateHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -1405,6 +1407,7 @@
                 });
             }
         }
+        InternalStateHandler.handleIntent(this, intent);
 
         TraceHelper.endSection("NEW_INTENT");
     }
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
new file mode 100644
index 0000000..a90ed36
--- /dev/null
+++ b/src/com/android/launcher3/states/InternalStateHandler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 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.states;
+
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.Launcher.OnResumeCallback;
+
+/**
+ * Utility class to sending state handling logic to Launcher from within the same process
+ */
+public abstract class InternalStateHandler extends Binder implements OnResumeCallback {
+
+    public static final String EXTRA_STATE_HANDLER = "launcher.state_handler";
+
+    public abstract void onNewIntent(Launcher launcher);
+
+    public static void handleIntent(Launcher launcher, Intent intent) {
+        IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
+        if (stateBinder instanceof InternalStateHandler) {
+            InternalStateHandler handler = (InternalStateHandler) stateBinder;
+            launcher.setOnResumeCallback(handler);
+            handler.onNewIntent(launcher);
+        }
+        intent.getExtras().remove(EXTRA_STATE_HANDLER);
+    }
+}