Two state swipe interaction fixes

> Loading the recents plan before showing recents and always starting with page 0
> Fixing thumbnail not getting drawn when layout happens after task plan is loaded
> Disabling two state swipe when touch service is not connected
> When using swite gesture, once user triggers the overview mode, he cannot land
  in NORMAL state in that gesture

Change-Id: I7a2e83318af6e98dcc64849690fcba6dc8f71f77
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
index 435d57e..410a36f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
@@ -29,7 +29,6 @@
 import android.support.animation.SpringAnimation;
 import android.util.Log;
 import android.view.MotionEvent;
-import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
@@ -42,7 +41,6 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
@@ -50,6 +48,10 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.FloatRange;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TouchInteractionService;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 
 import java.util.ArrayList;
 
@@ -86,6 +88,8 @@
     private static final int FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE = 1 << 0;
     private static final int FLAG_OVERVIEW_DISABLED_FLING = 1 << 1;
     private static final int FLAG_OVERVIEW_DISABLED_CANCEL_STATE = 1 << 2;
+    private static final int FLAG_RECENTS_PLAN_LOADING = 1 << 3;
+    private static final int FLAG_OVERVIEW_DISABLED = 1 << 4;
 
     private final Launcher mLauncher;
     private final SwipeDetector mDetector;
@@ -98,9 +102,10 @@
     private TaggedAnimatorSetBuilder mTaggedAnimatorSetBuilder;
     private AnimatorSet mQuickOverviewAnimation;
     private boolean mAnimatingToOverview;
-    private TwoStateAnimationController mTwoStateAnimationController;
+    private CroppedAnimationController mCroppedAnimationController;
 
     private AnimatorPlaybackController mCurrentAnimation;
+    private LauncherState mFromState;
     private LauncherState mToState;
 
     private float mStartProgress;
@@ -240,11 +245,27 @@
                     + MAX_PROGRESS_TO_OVERVIEW - MIN_PROGRESS_TO_OVERVIEW;
 
             // Build current animation
+            mFromState = mLauncher.getStateManager().getState();
             mToState = mLauncher.isInState(ALL_APPS) ? NORMAL : ALL_APPS;
             mTaggedAnimatorSetBuilder = new TaggedAnimatorSetBuilder();
             mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(
                     mToState, mTaggedAnimatorSetBuilder, maxAccuracy);
 
+            if (TouchInteractionService.isConnected()) {
+                // Load recents plan
+                RecentsModel recentsModel = RecentsModel.getInstance(mLauncher);
+                if (recentsModel.getLastLoadPlan() != null) {
+                    onRecentsPlanLoaded(recentsModel.getLastLoadPlan());
+                } else {
+                    mDragPauseDetector.addDisabledFlags(FLAG_RECENTS_PLAN_LOADING);
+                }
+                // Reload again so that we get the latest list
+                // TODO: Use callback instead of polling everytime
+                recentsModel.loadTasks(-1, this::onRecentsPlanLoaded);
+            } else {
+                mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED);
+            }
+
             mCurrentAnimation.getTarget().addListener(this);
             mStartProgress = 0;
             mProgressMultiplier = (mLauncher.isInState(ALL_APPS) ? 1 : -1) / range;
@@ -262,6 +283,14 @@
         }
     }
 
+    private void onRecentsPlanLoaded(RecentsTaskLoadPlan plan) {
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        recentsView.update(plan);
+        recentsView.initToPage(0);
+
+        mDragPauseDetector.clearDisabledFlags(FLAG_RECENTS_PLAN_LOADING);
+    }
+
     private float getShiftRange() {
         return mLauncher.getAllAppsController().getShiftRange();
     }
@@ -287,16 +316,11 @@
 
     @Override
     public void onDragEnd(float velocity, boolean fling) {
-        if (!fling && mDragPauseDetector.isEnabled() && mDragPauseDetector.isTriggered()) {
-            snapToOverview(velocity);
-            return;
-        }
-
         mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_FLING);
 
         final long animationDuration;
         final int logAction;
-        final LauncherState targetState;
+        LauncherState targetState;
         final float progress = mCurrentAnimation.getProgressFraction();
 
         if (fling) {
@@ -317,7 +341,7 @@
                 targetState = mToState;
                 animationDuration = SwipeDetector.calculateDuration(velocity, 1 - progress);
             } else {
-                targetState = mToState == ALL_APPS ? NORMAL : ALL_APPS;
+                targetState = mFromState;
                 animationDuration = SwipeDetector.calculateDuration(velocity, progress);
             }
         }
@@ -328,7 +352,13 @@
                 h.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
             }
         }
-        mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
+        mCurrentAnimation.setEndAction(() -> {
+            LauncherState finalState = targetState;
+            if (mDragPauseDetector.isTriggered() && targetState == NORMAL) {
+                finalState = OVERVIEW;
+            }
+            onSwipeInteractionCompleted(finalState, logAction);
+        });
 
         float nextFrameProgress = Utilities.boundToRange(
                 progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
@@ -341,7 +371,7 @@
     }
 
     private void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
-        if (targetState == mToState) {
+        if (targetState != mFromState) {
             // Transition complete. log the action
             mLauncher.getUserEventDispatcher().logActionOnContainer(logAction,
                     mToState == ALL_APPS ? Direction.UP : Direction.DOWN,
@@ -354,33 +384,6 @@
         mLauncher.getStateManager().goToState(targetState, false /* animated */);
     }
 
-    private void snapToOverview(float velocity) {
-        mAnimatingToOverview = true;
-
-        final float progress = mCurrentAnimation.getProgressFraction();
-        float endProgress = mToState == NORMAL ? 1f : 0f;
-        long animationDuration = SwipeDetector.calculateDuration(
-                velocity, Math.abs(endProgress - progress));
-        float nextFrameProgress = Utilities.boundToRange(
-                progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
-
-        mCurrentAnimation.setEndAction(() -> {
-            // TODO: Add logging
-            clearState();
-            mLauncher.getStateManager().goToState(OVERVIEW, true /* animated */);
-        });
-
-        if (mTwoStateAnimationController != null) {
-            mTwoStateAnimationController.goBackToStart(endProgress);
-        }
-
-        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
-        anim.setFloatValues(nextFrameProgress, endProgress);
-        anim.setDuration(animationDuration);
-        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
-        anim.start();
-    }
-
     private void onDragPauseDetected() {
         final ValueAnimator twoStepAnimator = ValueAnimator.ofFloat(0, 1);
         twoStepAnimator.setDuration(mCurrentAnimation.getDuration());
@@ -409,33 +412,29 @@
         mQuickOverviewAnimation.start();
     }
 
-    private void onQuickOverviewAnimationComplete(ValueAnimator twoStepAnimator) {
+    private void onQuickOverviewAnimationComplete(ValueAnimator animator) {
         if (mAnimatingToOverview) {
             return;
         }
 
-        // The remaining state handlers are on the OVERVIEW state. Create two animations, one
-        // towards the NORMAL state and one towards ALL_APPS state and control them based on the
-        // swipe progress.
+        // For the remainder to the interaction, the user can either go to the ALL_APPS state or
+        // the OVERVIEW state.
+        // The remaining state handlers are on the OVERVIEW state. Create one animation towards the
+        // ALL_APPS state and only call it when the user moved above the current range.
         AnimationConfig config = new AnimationConfig();
         config.duration = (long) (2 * getShiftRange());
         config.userControlled = true;
 
-        LauncherState fromState = mToState == ALL_APPS ? NORMAL : ALL_APPS;
-        AnimatorSetBuilder builderToTargetState = new AnimatorSetBuilder();
-        AnimatorSetBuilder builderToSourceState = new AnimatorSetBuilder();
-
+        AnimatorSetBuilder builderToAllAppsState = new AnimatorSetBuilder();
         StateHandler[] handlers = mLauncher.getStateManager().getStateHandlers();
         for (int i = OTHER_HANDLERS_START_INDEX; i < handlers.length; i++) {
-            handlers[i].setStateWithAnimation(mToState, builderToTargetState, config);
-            handlers[i].setStateWithAnimation(fromState, builderToSourceState, config);
+            handlers[i].setStateWithAnimation(ALL_APPS, builderToAllAppsState, config);
         }
 
-        mTwoStateAnimationController = new TwoStateAnimationController(
-                AnimatorPlaybackController.wrap(builderToSourceState.build(), config.duration),
-                AnimatorPlaybackController.wrap(builderToTargetState.build(), config.duration),
-                twoStepAnimator.getAnimatedFraction());
-        twoStepAnimator.addUpdateListener(mTwoStateAnimationController);
+        mCroppedAnimationController = new CroppedAnimationController(
+                AnimatorPlaybackController.wrap(builderToAllAppsState.build(), config.duration),
+                new FloatRange(animator.getAnimatedFraction(), mToState == ALL_APPS ? 1 : 0));
+        animator.addUpdateListener(mCroppedAnimationController);
     }
 
     private void clearState() {
@@ -450,69 +449,49 @@
             mQuickOverviewAnimation.cancel();
             mQuickOverviewAnimation = null;
         }
-        mTwoStateAnimationController = null;
+        mCroppedAnimationController = null;
         mAnimatingToOverview = false;
 
         mDetector.finishedScrolling();
     }
 
     /**
-     * {@link AnimatorUpdateListener} which interpolates two animations based the progress
+     * {@link AnimatorUpdateListener} which controls another animation for a fraction of range
      */
-    private static class TwoStateAnimationController implements AnimatorUpdateListener {
+    private static class CroppedAnimationController implements AnimatorUpdateListener {
 
-        private final AnimatorPlaybackController mControllerTowardsStart;
-        private final AnimatorPlaybackController mControllerTowardsEnd;
+        private final AnimatorPlaybackController mTarget;
+        private final FloatRange mRange;
 
-        private Interpolator mInterpolator = Interpolators.LINEAR;
-        private float mStartFraction;
-        private float mLastFraction;
-
-        TwoStateAnimationController(AnimatorPlaybackController controllerTowardsStart,
-                AnimatorPlaybackController controllerTowardsEnd, float startFraction) {
-            mControllerTowardsStart = controllerTowardsStart;
-            mControllerTowardsEnd = controllerTowardsEnd;
-            mLastFraction = mStartFraction = startFraction;
+        CroppedAnimationController(AnimatorPlaybackController target, FloatRange range) {
+            mTarget = target;
+            mRange = range;
         }
 
+
         @Override
         public void onAnimationUpdate(ValueAnimator valueAnimator) {
-            mLastFraction = mInterpolator.getInterpolation(valueAnimator.getAnimatedFraction());
-            if (mLastFraction > mStartFraction) {
-                if (mStartFraction >= 1) {
-                    mControllerTowardsEnd.setPlayFraction(0);
-                } else {
-                    mControllerTowardsEnd.setPlayFraction(
-                            (mLastFraction - mStartFraction) / (1 - mStartFraction));
-                }
-            } else {
-                if (mStartFraction <= 0) {
-                    mControllerTowardsStart.setPlayFraction(0);
-                } else {
-                    mControllerTowardsStart.setPlayFraction(
-                            (mStartFraction - mLastFraction) / mStartFraction);
-                }
-            }
-        }
+            float fraction = valueAnimator.getAnimatedFraction();
 
-        /**
-         * Changes the interpolator such that from this point ({@link #mLastFraction}), the
-         * animation run towards {@link #mStartFraction}. This allows us to animate the UI back
-         * to the original point.
-         * @param endFraction expected end point for this animation. Should either be 0 or 1.
-         */
-        public void goBackToStart(float endFraction) {
-            if (mLastFraction == mStartFraction || mLastFraction == endFraction) {
-                mInterpolator = (v) -> mStartFraction;
-            } else if (mLastFraction > mStartFraction && endFraction < mStartFraction) {
-                mInterpolator = (v) -> Math.max(v, mStartFraction);
-            } else if (mLastFraction < mStartFraction && endFraction > mStartFraction) {
-                mInterpolator = (v) -> Math.min(mStartFraction, v);
+            if (mRange.start < mRange.end) {
+                if (fraction <= mRange.start) {
+                    mTarget.setPlayFraction(0);
+                } else if (fraction >= mRange.end) {
+                    mTarget.setPlayFraction(1);
+                } else {
+                    mTarget.setPlayFraction((fraction - mRange.start) / (mRange.end - mRange.start));
+                }
+            } else if (mRange.start > mRange.end) {
+                if (fraction >= mRange.start) {
+                    mTarget.setPlayFraction(0);
+                } else if (fraction <= mRange.end) {
+                    mTarget.setPlayFraction(1);
+                } else {
+                    mTarget.setPlayFraction((fraction - mRange.start) / (mRange.end - mRange.start));
+                }
             } else {
-                final float start = mLastFraction;
-                final float range = endFraction - mLastFraction;
-                mInterpolator = (v) ->
-                        SwipeDetector.interpolate(start, mStartFraction, (v - start) / range);
+                // mRange.start == mRange.end
+                mTarget.setPlayFraction(0);
             }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index 095b445..09fd8f0 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.states.InternalStateHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.views.AllAppsScrim;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
@@ -97,6 +98,7 @@
     private RecentsView mRecentsView;
     private RecentsViewStateController mStateController;
     private Hotseat mHotseat;
+    private AllAppsScrim mAllAppsScrim;
     private RecentsTaskLoadPlan mLoadPlan;
 
     private boolean mLauncherReady;
@@ -182,6 +184,7 @@
         mRecentsView = mLauncher.getOverviewPanel();
         mStateController = mRecentsView.getStateController();
         mHotseat = mLauncher.getHotseat();
+        mAllAppsScrim = mLauncher.findViewById(R.id.all_apps_scrim);
 
         // Optimization
         mLauncher.getAppsView().setVisibility(View.GONE);
@@ -222,7 +225,9 @@
         float shift = mCurrentShift.value * mActivityMultiplier.value;
         int hotseatSize = getHotseatSize();
 
-        mHotseat.setTranslationY((1 - shift) * hotseatSize);
+        float hotseatTranslation = (1 - shift) * hotseatSize;
+        mHotseat.setTranslationY(hotseatTranslation);
+        mAllAppsScrim.setTranslationY(hotseatTranslation);
 
         mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
 
@@ -324,6 +329,7 @@
     private void cleanupLauncher() {
         // TODO: These should be done as part of ActivityOptions#OnAnimationStarted
         mHotseat.setTranslationY(0);
+        mAllAppsScrim.setTranslationY(0);
         mLauncher.setOnResumeCallback(() -> mDragView.close(false));
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
new file mode 100644
index 0000000..112f156
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -0,0 +1,104 @@
+/*
+ * 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.quickstep;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions;
+import com.android.systemui.shared.recents.model.RecentsTaskLoader;
+import com.android.systemui.shared.system.BackgroundExecutor;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+/**
+ * Singleton class to load and manage recents model.
+ */
+public class RecentsModel {
+
+    // We do not need any synchronization for this variable as its only written on UI thread.
+    private static RecentsModel INSTANCE;
+
+    public static RecentsModel getInstance(final Context context) {
+        if (INSTANCE == null) {
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                INSTANCE = new RecentsModel(context.getApplicationContext());
+            } else {
+                try {
+                    return new MainThreadExecutor().submit(
+                            () -> RecentsModel.getInstance(context)).get();
+                } catch (InterruptedException|ExecutionException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+        return INSTANCE;
+    }
+
+    private final Context mContext;
+    private final RecentsTaskLoader mRecentsTaskLoader;
+    private final MainThreadExecutor mMainThreadExecutor;
+
+    private RecentsTaskLoadPlan mLastLoadPlan;
+    private RecentsModel(Context context) {
+        mContext = context;
+
+        Resources res = context.getResources();
+        mRecentsTaskLoader = new RecentsTaskLoader(mContext,
+                res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize),
+                res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
+        mRecentsTaskLoader.startLoader(mContext);
+
+        mMainThreadExecutor = new MainThreadExecutor();
+    }
+
+    public RecentsTaskLoader getRecentsTaskLoader() {
+        return mRecentsTaskLoader;
+    }
+
+    /**
+     * Preloads the task plan
+     * @param taskId The running task id or -1
+     * @param callback The callback to receive the task plan once its complete or null. This is
+     *                always called on the UI thread.
+     */
+    public void loadTasks(int taskId, Consumer<RecentsTaskLoadPlan> callback) {
+        BackgroundExecutor.get().submit(() -> {
+            // Preload the plan
+            RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext);
+            PreloadOptions opts = new PreloadOptions();
+            opts.loadTitles = false;
+            loadPlan.preloadPlan(opts, mRecentsTaskLoader, taskId, UserHandle.myUserId());
+            // Set the load plan on UI thread
+            mMainThreadExecutor.execute(() -> {
+                mLastLoadPlan = loadPlan;
+                if (callback != null) {
+                    callback.accept(loadPlan);
+                }
+            });
+        });
+    }
+
+    public RecentsTaskLoadPlan getLastLoadPlan() {
+        return mLastLoadPlan;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 6161858..00901c6 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -146,7 +146,8 @@
     }
 
     public void update(RecentsTaskLoadPlan loadPlan) {
-        final RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+        final RecentsTaskLoader loader = RecentsModel.getInstance(getContext())
+                .getRecentsTaskLoader();
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
         if (stack == null) {
             removeAllViews();
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 4a9bfea..3d4d451 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -173,4 +173,10 @@
         }
         invalidate();
     }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        updateThumbnailMatrix();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index f457a59..4321791 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -28,10 +28,8 @@
 import android.app.ActivityOptions;
 import android.app.Service;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -39,7 +37,6 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.Display;
@@ -52,14 +49,9 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MainThreadExecutor;
-import com.android.launcher3.R;
 import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions;
-import com.android.systemui.shared.recents.model.RecentsTaskLoader;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.WindowManagerWrapper;
@@ -74,8 +66,6 @@
 
     private static final String TAG = "TouchInteractionService";
 
-    private static RecentsTaskLoader sRecentsTaskLoader;
-
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
         @Override
@@ -93,12 +83,18 @@
             = this::handleTouchDownOnOtherActivity;
     private final Consumer<MotionEvent> mNoOpTouchConsumer = (ev) -> {};
 
+    private static boolean sConnected = false;
+
+    public static boolean isConnected() {
+        return sConnected;
+    }
+
     private ActivityManagerWrapper mAM;
     private RunningTaskInfo mRunningTask;
+    private RecentsModel mRecentsModel;
     private Intent mHomeIntent;
     private ComponentName mLauncher;
     private MotionEventQueue mEventQueue;
-    private MainThreadExecutor mMainThreadExecutor;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -117,6 +113,7 @@
     public void onCreate() {
         super.onCreate();
         mAM = ActivityManagerWrapper.getInstance();
+        mRecentsModel = RecentsModel.getInstance(this);
 
         mHomeIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
@@ -126,16 +123,15 @@
         mLauncher = new ComponentName(getPackageName(), info.activityInfo.name);
         mHomeIntent.setComponent(mLauncher);
 
-        Resources res = getResources();
-        if (sRecentsTaskLoader == null) {
-            sRecentsTaskLoader = new RecentsTaskLoader(this,
-                    res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize),
-                    res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
-            sRecentsTaskLoader.startLoader(this);
-        }
-
-        mMainThreadExecutor = new MainThreadExecutor();
         mEventQueue = new MotionEventQueue(Choreographer.getInstance(), this::handleMotionEvent);
+        mRecentsModel.loadTasks(-1, null);
+        sConnected = true;
+    }
+
+    @Override
+    public void onDestroy() {
+        sConnected = false;
+        super.onDestroy();
     }
 
     @Override
@@ -144,10 +140,6 @@
         return mMyBinder;
     }
 
-    public static RecentsTaskLoader getRecentsTaskLoader() {
-        return sRecentsTaskLoader;
-    }
-
     private void handleMotionEvent(MotionEvent ev) {
         if (ev.getActionMasked() == ACTION_DOWN) {
             mRunningTask = mAM.getRunningTask();
@@ -255,12 +247,9 @@
         final NavBarSwipeInteractionHandler handler =
                 new NavBarSwipeInteractionHandler(mRunningTask, this);
 
-        // Preload and start the recents activity on a background thread
-        final Context context = this;
-        final RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(context);
-        final int taskId = mRunningTask.id;
         TraceHelper.partitionSection("TouchInt", "Thershold crossed ");
 
+        // Start the recents activity on a background thread
         BackgroundExecutor.get().submit(() -> {
             // Get the snap shot before
             handler.setTaskSnapshot(getCurrentTaskSnapshot());
@@ -275,15 +264,10 @@
                     ActivityOptions.makeCustomAnimation(this, 0, 0), UserHandle.myUserId(),
                     null, null);
              */
-
-            // Preload the plan
-            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
-            PreloadOptions opts = new PreloadOptions();
-            opts.loadTitles = false;
-            loadPlan.preloadPlan(opts, loader, taskId, UserHandle.myUserId());
-            // Set the load plan on UI thread
-            mMainThreadExecutor.execute(() -> handler.setRecentsTaskLoadPlan(loadPlan));
         });
+
+        // Preload the plan
+        mRecentsModel.loadTasks(mRunningTask.id, handler::setRecentsTaskLoadPlan);
         mInteractionHandler = handler;
     }