Merge "Changing min sdk to 25" into ub-launcher3-master
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index 50af4a1..fb1828b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 
 import android.animation.Animator;
@@ -281,6 +282,9 @@
                 }
             });
         }
+        if (QUICKSTEP_SPRINGS.get()) {
+            mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
+        }
         anim.start();
     }
 
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index d6a7f21..fbb3618 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -23,7 +23,8 @@
 import static com.android.launcher3.LauncherState.FAST_OVERVIEW;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS_SPRING;
+import static com.android.launcher3.allapps.AllAppsTransitionController.SPRING_DAMPING_RATIO;
+import static com.android.launcher3.allapps.AllAppsTransitionController.SPRING_STIFFNESS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL;
@@ -394,9 +395,9 @@
         }
 
         private Animator createShelfAnim(Launcher activity, float ... progressValues) {
-            Animator shiftAnim = new SpringObjectAnimator(activity.getAllAppsController(),
-                    ALL_APPS_PROGRESS_SPRING, "allAppsSpringFromACH",
-                    activity.getAllAppsController().getShiftRange(), progressValues);
+            Animator shiftAnim = new SpringObjectAnimator<>(activity.getAllAppsController(),
+                    "allAppsSpringFromACH", activity.getAllAppsController().getShiftRange(),
+                    SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues);
             shiftAnim.setInterpolator(LINEAR);
             return shiftAnim;
         }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 923da7b..c6f293d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.uioverrides.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.quickstep.util.ClipAnimationHelper.TransformParams;
@@ -66,16 +67,19 @@
 import android.widget.ListView;
 
 import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAnimUtils.ViewProgressProperty;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.anim.SpringObjectAnimator;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -113,6 +117,10 @@
 
     private static final String TAG = RecentsView.class.getSimpleName();
 
+    public static final float SPRING_MIN_VISIBLE_CHANGE = 0.001f;
+    public static final float SPRING_DAMPING_RATIO = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
+    public static final float SPRING_STIFFNESS = SpringForce.STIFFNESS_LOW;
+
     public static final FloatProperty<RecentsView> CONTENT_ALPHA =
             new FloatProperty<RecentsView>("contentAlpha") {
                 @Override
@@ -941,8 +949,15 @@
 
     private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
         addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
-        addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
-                duration, LINEAR, anim);
+        if (QUICKSTEP_SPRINGS.get() && taskView instanceof TaskView)
+            addAnim(new SpringObjectAnimator<>(new ViewProgressProperty(taskView,
+                            View.TRANSLATION_Y), "taskViewTransY", SPRING_MIN_VISIBLE_CHANGE,
+                            SPRING_DAMPING_RATIO, SPRING_STIFFNESS, 0, -taskView.getHeight()),
+                    duration, LINEAR, anim);
+        else {
+            addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
+                    duration, LINEAR, anim);
+        }
     }
 
     private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener,
@@ -1012,8 +1027,16 @@
                 }
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
-                    addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff),
-                            duration, ACCEL, anim);
+                    if (QUICKSTEP_SPRINGS.get() && child instanceof TaskView) {
+                        addAnim(new SpringObjectAnimator<>(
+                                new ViewProgressProperty(child, View.TRANSLATION_X),
+                                "taskViewTransX", SPRING_MIN_VISIBLE_CHANGE, SPRING_DAMPING_RATIO,
+                                SPRING_STIFFNESS, 0, scrollDiff), duration, ACCEL, anim);
+                    } else {
+                        addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration,
+                                ACCEL, anim);
+                    }
+
                     needsCurveUpdates = true;
                 }
             }
@@ -1094,7 +1117,7 @@
         return pendingAnimation;
     }
 
-    private static void addAnim(ObjectAnimator anim, long duration,
+    private static void addAnim(Animator anim, long duration,
             TimeInterpolator interpolator, AnimatorSet set) {
         anim.setDuration(duration).setInterpolator(interpolator);
         set.play(anim);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTests.java b/quickstep/tests/src/com/android/quickstep/TaplTests.java
deleted file mode 100644
index 347b7ac..0000000
--- a/quickstep/tests/src/com/android/quickstep/TaplTests.java
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * 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 static androidx.test.InstrumentationRegistry.getInstrumentation;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.content.Intent;
-import android.os.RemoteException;
-import android.util.Log;
-
-import androidx.test.filters.LargeTest;
-import androidx.test.runner.AndroidJUnit4;
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.Until;
-
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
-import com.android.launcher3.tapl.AllApps;
-import com.android.launcher3.tapl.AllAppsFromOverview;
-import com.android.launcher3.tapl.AppIcon;
-import com.android.launcher3.tapl.Background;
-import com.android.launcher3.tapl.Overview;
-import com.android.launcher3.tapl.OverviewTask;
-import com.android.launcher3.tapl.TestHelpers;
-import com.android.launcher3.tapl.Widgets;
-import com.android.launcher3.tapl.Workspace;
-import com.android.launcher3.views.OptionsPopupView;
-import com.android.launcher3.widget.WidgetsFullSheet;
-import com.android.launcher3.widget.WidgetsRecyclerView;
-import com.android.quickstep.QuickStepOnOffRule.QuickstepOnOff;
-import com.android.quickstep.views.RecentsView;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestWatcher;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public class TaplTests extends AbstractQuickStepTest {
-    private static final String TAG = "TaplTests";
-
-    private static int sScreenshotCount = 0;
-
-    @Rule
-    public TestWatcher mFailureWatcher = new TestWatcher() {
-        private void dumpViewHierarchy() {
-            final ByteArrayOutputStream stream = new ByteArrayOutputStream();
-            try {
-                mDevice.dumpWindowHierarchy(stream);
-                stream.flush();
-                stream.close();
-                for (String line : stream.toString().split("\\r?\\n")) {
-                    Log.e(TaplTests.TAG, line.trim());
-                }
-            } catch (IOException e) {
-                Log.e(TaplTests.TAG, "error dumping XML to logcat", e);
-            }
-        }
-
-        @Override
-        protected void failed(Throwable e, Description description) {
-            if (mDevice == null) return;
-            final String pathname = getInstrumentation().getTargetContext().
-                    getFilesDir().getPath() + "/TaplTestScreenshot" + sScreenshotCount++ + ".png";
-            Log.e(TaplTests.TAG, "Failed test " + description.getMethodName() +
-                    ", screenshot will be saved to " + pathname +
-                    ", track trace is below, UI object dump is further below:\n" +
-                    Log.getStackTraceString(e));
-            dumpViewHierarchy();
-            mDevice.takeScreenshot(new File(pathname));
-        }
-    };
-
-    @Before
-    public void setUp() throws Exception {
-        super.setUp();
-
-        clearLauncherData();
-
-        mLauncher.pressHome();
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
-        waitForResumed("Launcher internal state is still Background");
-    }
-
-    private boolean isInState(LauncherState state) {
-        if (!TestHelpers.isInLauncherProcess()) return true;
-        return getFromLauncher(launcher -> launcher.getStateManager().getState() == state);
-    }
-
-    // Please don't add negative test cases for methods that fail only after a long wait.
-    private void expectFail(String message, Runnable action) {
-        boolean failed = false;
-        try {
-            action.run();
-        } catch (AssertionError e) {
-            failed = true;
-        }
-        assertTrue(message, failed);
-    }
-
-    private boolean isWorkspaceScrollable(Launcher launcher) {
-        return launcher.getWorkspace().getPageCount() > 1;
-    }
-
-    private boolean isInBackground(Launcher launcher) {
-        return !launcher.hasBeenResumed();
-    }
-
-    private void startTestApps() throws Exception {
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_MESSAGING));
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS));
-
-        executeOnLauncher(launcher -> assertTrue(
-                "Launcher activity is the top activity; expecting another activity to be the top "
-                        + "one",
-                isInBackground(launcher)));
-    }
-
-    private int getCurrentWorkspacePage(Launcher launcher) {
-        return launcher.getWorkspace().getCurrentPage();
-    }
-
-    private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
-        return WidgetsFullSheet.getWidgetsView(launcher);
-    }
-
-    @Test
-    public void testDevicePressMenu() throws Exception {
-        mDevice.pressMenu();
-        mDevice.waitForIdle();
-        executeOnLauncher(
-                launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
-                        OptionsPopupView.getOptionsPopup(launcher) != null));
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
-        mDevice.pressRecentApps();
-        waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
-
-        assertNotNull("getOverview() returned null", mLauncher.getOverview());
-    }
-
-    private void runAllAppsTest(AllApps allApps) throws Exception {
-        assertNotNull("allApps parameter is null", allApps);
-
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-
-        // Test flinging forward and backward.
-        executeOnLauncher(launcher -> assertEquals("All Apps started in already scrolled state", 0,
-                getAllAppsScroll(launcher)));
-
-        allApps.flingForward();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-        final Integer flingForwardY = getFromLauncher(launcher -> getAllAppsScroll(launcher));
-        executeOnLauncher(
-                launcher -> assertTrue("flingForward() didn't scroll App Apps", flingForwardY > 0));
-
-        allApps.flingBackward();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-        final Integer flingBackwardY = getFromLauncher(launcher -> getAllAppsScroll(launcher));
-        executeOnLauncher(launcher -> assertTrue("flingBackward() didn't scroll App Apps",
-                flingBackwardY < flingForwardY));
-
-        // Test scrolling down to YouTube.
-        assertNotNull("All apps: can't fine YouTube", allApps.getAppIcon("YouTube"));
-        // Test scrolling up to Camera.
-        assertNotNull("All apps: can't fine Camera", allApps.getAppIcon("Camera"));
-        // Test failing to find a non-existing app.
-        final AllApps allAppsFinal = allApps;
-        expectFail("All apps: could find a non-existing app",
-                () -> allAppsFinal.getAppIcon("NO APP"));
-
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-    }
-
-    private int getAllAppsScroll(Launcher launcher) {
-        return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testAllAppsFromHome() throws Exception {
-        // Test opening all apps
-        assertNotNull("switchToAllApps() returned null",
-                mLauncher.getWorkspace().switchToAllApps());
-
-        runAllAppsTest(mLauncher.getAllApps());
-
-        // Testing pressHome.
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-        assertNotNull("pressHome returned null", mLauncher.pressHome());
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
-        assertNotNull("getHome returned null", mLauncher.getWorkspace());
-    }
-
-    @Test
-    @QuickstepOnOff
-    @PortraitLandscape
-    public void testWorkspaceSwitchToAllApps() {
-        assertNotNull("switchToAllApps() returned null",
-                mLauncher.getWorkspace().switchToAllApps());
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-    }
-
-    @Test
-    public void testAllAppsFromOverview() throws Exception {
-        // Test opening all apps from Overview.
-        assertNotNull("switchToAllApps() returned null",
-                mLauncher.getWorkspace().switchToOverview().switchToAllApps());
-
-        runAllAppsTest(mLauncher.getAllAppsFromOverview());
-    }
-
-    @Test
-    public void testWorkspace() throws Exception {
-        final Workspace workspace = mLauncher.getWorkspace();
-
-        // Test that ensureWorkspaceIsScrollable adds a page by dragging an icon there.
-        executeOnLauncher(launcher -> assertFalse("Initial workspace state is scrollable",
-                isWorkspaceScrollable(launcher)));
-        assertNull("Messages app was found on empty workspace",
-                workspace.tryGetWorkspaceAppIcon("Messages"));
-
-        workspace.ensureWorkspaceIsScrollable();
-
-        executeOnLauncher(
-                launcher -> assertEquals("Ensuring workspace scrollable didn't switch to page #1",
-                        1, getCurrentWorkspacePage(launcher)));
-        executeOnLauncher(
-                launcher -> assertTrue("ensureScrollable didn't make workspace scrollable",
-                        isWorkspaceScrollable(launcher)));
-        assertNotNull("ensureScrollable didn't add Messages app",
-                workspace.tryGetWorkspaceAppIcon("Messages"));
-
-        // Test flinging workspace.
-        workspace.flingBackward();
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
-        executeOnLauncher(
-                launcher -> assertEquals("Flinging back didn't switch workspace to page #0",
-                        0, getCurrentWorkspacePage(launcher)));
-
-        workspace.flingForward();
-        executeOnLauncher(
-                launcher -> assertEquals("Flinging forward didn't switch workspace to page #1",
-                        1, getCurrentWorkspacePage(launcher)));
-        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
-
-        // Test starting a workspace app.
-        final AppIcon app = workspace.tryGetWorkspaceAppIcon("Messages");
-        assertNotNull("No Messages app in workspace", app);
-        assertNotNull("AppIcon.launch returned null",
-                app.launch(resolveSystemApp(Intent.CATEGORY_APP_MESSAGING)));
-        executeOnLauncher(launcher -> assertTrue(
-                "Launcher activity is the top activity; expecting another activity to be the top "
-                        + "one",
-                isInBackground(launcher)));
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testOverview() throws Exception {
-        startTestApps();
-        Overview overview = mLauncher.pressHome().switchToOverview();
-        assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
-        executeOnLauncher(
-                launcher -> assertTrue("Don't have at least 3 tasks", getTaskCount(launcher) >= 3));
-
-        // Test flinging forward and backward.
-        executeOnLauncher(launcher -> assertEquals("Current task in Overview is not 0",
-                0, getCurrentOverviewPage(launcher)));
-
-        overview.flingForward();
-        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
-        final Integer currentTaskAfterFlingForward = getFromLauncher(
-                launcher -> getCurrentOverviewPage(launcher));
-        executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0",
-                currentTaskAfterFlingForward > 0));
-
-        overview.flingBackward();
-        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
-        executeOnLauncher(launcher -> assertTrue("Flinging back in Overview did nothing",
-                getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward));
-
-        // Test opening a task.
-        OverviewTask task = mLauncher.pressHome().switchToOverview().getCurrentTask();
-        assertNotNull("overview.getCurrentTask() returned null (1)", task);
-        assertNotNull("OverviewTask.open returned null", task.open());
-        assertTrue("Contacts app didn't open from Overview", mDevice.wait(Until.hasObject(
-                By.pkg(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS)).depth(0)),
-                LONG_WAIT_TIME_MS));
-        executeOnLauncher(launcher -> assertTrue(
-                "Launcher activity is the top activity; expecting another activity to be the top "
-                        + "one",
-                isInBackground(launcher)));
-
-        // Test dismissing a task.
-        overview = mLauncher.pressHome().switchToOverview();
-        assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
-        final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher));
-        task = overview.getCurrentTask();
-        assertNotNull("overview.getCurrentTask() returned null (2)", task);
-        task.dismiss();
-        executeOnLauncher(
-                launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview",
-                        numTasks - 1, getTaskCount(launcher)));
-
-        if (!TestHelpers.isInLauncherProcess() ||
-                getFromLauncher(launcher -> !launcher.getDeviceProfile().isLandscape)) {
-            // Test switching to all apps and back.
-            final AllAppsFromOverview allApps = overview.switchToAllApps();
-            assertNotNull("overview.switchToAllApps() returned null (1)", allApps);
-            assertTrue("Launcher internal state is not All Apps (1)",
-                    isInState(LauncherState.ALL_APPS));
-
-            overview = allApps.switchBackToOverview();
-            assertNotNull("allApps.switchBackToOverview() returned null", overview);
-            assertTrue("Launcher internal state didn't switch to Overview",
-                    isInState(LauncherState.OVERVIEW));
-
-            // Test UIDevice.pressBack()
-            overview.switchToAllApps();
-            assertNotNull("overview.switchToAllApps() returned null (2)", allApps);
-            assertTrue("Launcher internal state is not All Apps (2)",
-                    isInState(LauncherState.ALL_APPS));
-            mDevice.pressBack();
-            mLauncher.getOverview();
-        }
-
-        // Test UIDevice.pressHome, once we are in AllApps.
-        mDevice.pressHome();
-        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
-    }
-
-    private int getCurrentOverviewPage(Launcher launcher) {
-        return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
-    }
-
-    private int getTaskCount(Launcher launcher) {
-        return launcher.<RecentsView>getOverviewPanel().getTaskViewCount();
-    }
-
-    private void runIconLaunchFromAllAppsTest(AllApps allApps) throws Exception {
-        final AppIcon app = allApps.getAppIcon("Calculator");
-        assertNotNull("AppIcon.launch returned null", app.launch(
-                resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)));
-        executeOnLauncher(launcher -> assertTrue(
-                "Launcher activity is the top activity; expecting another activity to be the top "
-                        + "one",
-                isInBackground(launcher)));
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testAppIconLaunchFromAllAppsFromHome() throws Exception {
-        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-
-        runIconLaunchFromAllAppsTest(allApps);
-    }
-
-    @Test
-    public void testAppIconLaunchFromAllAppsFromOverview() throws Exception {
-        final AllApps allApps =
-                mLauncher.getWorkspace().switchToOverview().switchToAllApps();
-        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
-
-        runIconLaunchFromAllAppsTest(allApps);
-    }
-
-    @Test
-    @PortraitLandscape
-    public void testWidgets() throws Exception {
-        // Test opening widgets.
-        executeOnLauncher(launcher ->
-                assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
-        Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
-        assertNotNull("openAllWidgets() returned null", widgets);
-        widgets = mLauncher.getAllWidgets();
-        assertNotNull("getAllWidgets() returned null", widgets);
-        executeOnLauncher(launcher ->
-                assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
-        executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
-                0, getWidgetsScroll(launcher)));
-
-        // Test flinging widgets.
-        widgets.flingForward();
-        Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
-        executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
-                flingForwardY > 0));
-
-        widgets.flingBackward();
-        executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
-                getWidgetsScroll(launcher) < flingForwardY));
-
-        mLauncher.pressHome();
-        waitForLauncherCondition("Widgets were not closed",
-                launcher -> getWidgetsView(launcher) == null);
-    }
-
-    private int getWidgetsScroll(Launcher launcher) {
-        return getWidgetsView(launcher).getCurrentScrollY();
-    }
-
-    @Test
-    @QuickstepOnOff
-    @PortraitLandscape
-    public void testSwitchToOverview() throws Exception {
-        assertNotNull("Workspace.switchToOverview() returned null",
-                mLauncher.pressHome().switchToOverview());
-        assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
-    }
-
-    @Test
-    @QuickstepOnOff
-    @PortraitLandscape
-    public void testBackground() throws Exception {
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
-        final Background background = mLauncher.getBackground();
-        assertNotNull("Launcher.getBackground() returned null", background);
-        executeOnLauncher(launcher -> assertTrue(
-                "Launcher activity is the top activity; expecting another activity to be the top "
-                        + "one",
-                isInBackground(launcher)));
-
-        assertNotNull("Background.switchToOverview() returned null", background.switchToOverview());
-        assertTrue("Launcher internal state didn't switch to Overview",
-                isInState(LauncherState.OVERVIEW));
-    }
-}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
new file mode 100644
index 0000000..8f7cf92
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -0,0 +1,224 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Intent;
+import android.os.RemoteException;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Until;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.tapl.AllApps;
+import com.android.launcher3.tapl.AllAppsFromOverview;
+import com.android.launcher3.tapl.Background;
+import com.android.launcher3.tapl.Overview;
+import com.android.launcher3.tapl.OverviewTask;
+import com.android.launcher3.tapl.TestHelpers;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.quickstep.QuickStepOnOffRule.QuickstepOnOff;
+import com.android.quickstep.views.RecentsView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TaplTestsQuickstep extends AbstractQuickStepTest {
+    @Rule
+    public TestWatcher mFailureWatcher = new TaplTestsLauncher3.FailureWatcher(mDevice);
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        clearLauncherData();
+
+        mLauncher.pressHome();
+        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForResumed("Launcher internal state is still Background");
+    }
+
+    private void startTestApps() throws Exception {
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_MESSAGING));
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS));
+
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top "
+                        + "one",
+                isInBackground(launcher)));
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testPressRecentAppsLauncherAndGetOverview() throws RemoteException {
+        mDevice.pressRecentApps();
+        waitForState("Launcher internal state didn't switch to Overview", LauncherState.OVERVIEW);
+
+        assertNotNull("getOverview() returned null", mLauncher.getOverview());
+    }
+
+    @Test
+    @QuickstepOnOff
+    @PortraitLandscape
+    public void testWorkspaceSwitchToAllApps() {
+        assertNotNull("switchToAllApps() returned null",
+                mLauncher.getWorkspace().switchToAllApps());
+        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+    }
+
+    @Test
+    public void testAllAppsFromOverview() throws Exception {
+        // Test opening all apps from Overview.
+        assertNotNull("switchToAllApps() returned null",
+                mLauncher.getWorkspace().switchToOverview().switchToAllApps());
+
+        TaplTestsLauncher3.runAllAppsTest(this, mLauncher.getAllAppsFromOverview());
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testOverview() throws Exception {
+        startTestApps();
+        Overview overview = mLauncher.pressHome().switchToOverview();
+        assertTrue("Launcher internal state didn't switch to Overview",
+                isInState(LauncherState.OVERVIEW));
+        executeOnLauncher(
+                launcher -> assertTrue("Don't have at least 3 tasks", getTaskCount(launcher) >= 3));
+
+        // Test flinging forward and backward.
+        executeOnLauncher(launcher -> assertEquals("Current task in Overview is not 0",
+                0, getCurrentOverviewPage(launcher)));
+
+        overview.flingForward();
+        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
+        final Integer currentTaskAfterFlingForward = getFromLauncher(
+                launcher -> getCurrentOverviewPage(launcher));
+        executeOnLauncher(launcher -> assertTrue("Current task in Overview is still 0",
+                currentTaskAfterFlingForward > 0));
+
+        overview.flingBackward();
+        assertTrue("Launcher internal state is not Overview", isInState(LauncherState.OVERVIEW));
+        executeOnLauncher(launcher -> assertTrue("Flinging back in Overview did nothing",
+                getCurrentOverviewPage(launcher) < currentTaskAfterFlingForward));
+
+        // Test opening a task.
+        OverviewTask task = mLauncher.pressHome().switchToOverview().getCurrentTask();
+        assertNotNull("overview.getCurrentTask() returned null (1)", task);
+        assertNotNull("OverviewTask.open returned null", task.open());
+        assertTrue("Contacts app didn't open from Overview", mDevice.wait(Until.hasObject(
+                By.pkg(resolveSystemApp(Intent.CATEGORY_APP_CONTACTS)).depth(0)),
+                LONG_WAIT_TIME_MS));
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top "
+                        + "one",
+                isInBackground(launcher)));
+
+        // Test dismissing a task.
+        overview = mLauncher.pressHome().switchToOverview();
+        assertTrue("Launcher internal state didn't switch to Overview",
+                isInState(LauncherState.OVERVIEW));
+        final Integer numTasks = getFromLauncher(launcher -> getTaskCount(launcher));
+        task = overview.getCurrentTask();
+        assertNotNull("overview.getCurrentTask() returned null (2)", task);
+        task.dismiss();
+        executeOnLauncher(
+                launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview",
+                        numTasks - 1, getTaskCount(launcher)));
+
+        if (!TestHelpers.isInLauncherProcess() ||
+                getFromLauncher(launcher -> !launcher.getDeviceProfile().isLandscape)) {
+            // Test switching to all apps and back.
+            final AllAppsFromOverview allApps = overview.switchToAllApps();
+            assertNotNull("overview.switchToAllApps() returned null (1)", allApps);
+            assertTrue("Launcher internal state is not All Apps (1)",
+                    isInState(LauncherState.ALL_APPS));
+
+            overview = allApps.switchBackToOverview();
+            assertNotNull("allApps.switchBackToOverview() returned null", overview);
+            assertTrue("Launcher internal state didn't switch to Overview",
+                    isInState(LauncherState.OVERVIEW));
+
+            // Test UIDevice.pressBack()
+            overview.switchToAllApps();
+            assertNotNull("overview.switchToAllApps() returned null (2)", allApps);
+            assertTrue("Launcher internal state is not All Apps (2)",
+                    isInState(LauncherState.ALL_APPS));
+            mDevice.pressBack();
+            mLauncher.getOverview();
+        }
+
+        // Test UIDevice.pressHome, once we are in AllApps.
+        mDevice.pressHome();
+        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+    }
+
+    private int getCurrentOverviewPage(Launcher launcher) {
+        return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
+    }
+
+    private int getTaskCount(Launcher launcher) {
+        return launcher.<RecentsView>getOverviewPanel().getTaskViewCount();
+    }
+
+    @Test
+    public void testAppIconLaunchFromAllAppsFromOverview() throws Exception {
+        final AllApps allApps =
+                mLauncher.getWorkspace().switchToOverview().switchToAllApps();
+        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+
+        TaplTestsLauncher3.runIconLaunchFromAllAppsTest(this, allApps);
+    }
+
+    @Test
+    @QuickstepOnOff
+    @PortraitLandscape
+    public void testSwitchToOverview() throws Exception {
+        assertNotNull("Workspace.switchToOverview() returned null",
+                mLauncher.pressHome().switchToOverview());
+        assertTrue("Launcher internal state didn't switch to Overview",
+                isInState(LauncherState.OVERVIEW));
+    }
+
+    @Test
+    @QuickstepOnOff
+    @PortraitLandscape
+    public void testBackground() throws Exception {
+        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        final Background background = mLauncher.getBackground();
+        assertNotNull("Launcher.getBackground() returned null", background);
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top "
+                        + "one",
+                isInBackground(launcher)));
+
+        assertNotNull("Background.switchToOverview() returned null", background.switchToOverview());
+        assertTrue("Launcher internal state didn't switch to Overview",
+                isInState(LauncherState.OVERVIEW));
+    }
+}
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index aad3449..04f2b52 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -91,4 +91,24 @@
                     lp.height = height;
                 }
             };
+
+    public static class ViewProgressProperty implements ProgressInterface {
+        View mView;
+        Property<View, Float> mProperty;
+
+        public ViewProgressProperty(View view, Property<View, Float> property) {
+            mView = view;
+            mProperty = property;
+        }
+
+        @Override
+        public void setProgress(float progress) {
+            mProperty.set(mView, progress);
+        }
+
+        @Override
+        public float getProgress() {
+            return mProperty.get(mView);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index e8e93fe..bcb5eec 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -47,6 +47,9 @@
 public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener,
         ProgressInterface {
 
+    public static final float SPRING_DAMPING_RATIO = 0.9f;
+    public static final float SPRING_STIFFNESS = 600f;
+
     public static final Property<AllAppsTransitionController, Float> ALL_APPS_PROGRESS =
             new Property<AllAppsTransitionController, Float>(Float.class, "allAppsProgress") {
 
@@ -61,19 +64,6 @@
         }
     };
 
-    public static final FloatPropertyCompat<AllAppsTransitionController> ALL_APPS_PROGRESS_SPRING
-            = new FloatPropertyCompat<AllAppsTransitionController>("allAppsProgressSpring") {
-        @Override
-        public float getValue(AllAppsTransitionController controller) {
-            return controller.mProgress;
-        }
-
-        @Override
-        public void setValue(AllAppsTransitionController controller, float progress) {
-            controller.setProgress(progress);
-        }
-    };
-
     private AllAppsContainerView mAppsView;
     private ScrimView mScrimView;
 
@@ -191,8 +181,8 @@
         Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW
                 ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN)
                 : FAST_OUT_SLOW_IN;
-        Animator anim = new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS_SPRING,
-                "allAppsSpringFromAATC", 1f / mShiftRange, mProgress, targetProgress);
+        Animator anim = new SpringObjectAnimator<>(this, "allAppsSpringFromAATC", 1f / mShiftRange,
+                SPRING_DAMPING_RATIO, SPRING_STIFFNESS, mProgress, targetProgress);
         anim.setDuration(config.duration);
         anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator));
         anim.addListener(getProgressAnimatorListener());
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
index e4aec10..4f45c05 100644
--- a/src/com/android/launcher3/anim/SpringObjectAnimator.java
+++ b/src/com/android/launcher3/anim/SpringObjectAnimator.java
@@ -55,17 +55,27 @@
     private boolean mAnimatorEnded = false;
     private boolean mEnded = false;
 
-    private static final float SPRING_DAMPING_RATIO = 0.9f;
-    private static final float SPRING_STIFFNESS = 600f;
+    private static final FloatPropertyCompat<ProgressInterface> sFloatProperty =
+            new FloatPropertyCompat<ProgressInterface>("springObjectAnimator") {
+        @Override
+        public float getValue(ProgressInterface object) {
+            return object.getProgress();
+        }
 
-    public SpringObjectAnimator(T object, FloatPropertyCompat<T> floatProperty,
-            String name, float minimumVisibleChange, float... values) {
+        @Override
+        public void setValue(ProgressInterface object, float progress) {
+            object.setProgress(progress);
+        }
+    };
+
+    public SpringObjectAnimator(T object, String name, float minimumVisibleChange, float damping,
+            float stiffness, float... values) {
         mObject = object;
-        mSpring = new SpringAnimation(object, floatProperty);
+        mSpring = new SpringAnimation(object, sFloatProperty);
         mSpring.setMinimumVisibleChange(minimumVisibleChange);
         mSpring.setSpring(new SpringForce(0)
-                .setDampingRatio(SPRING_DAMPING_RATIO)
-                .setStiffness(SPRING_STIFFNESS));
+                .setDampingRatio(damping)
+                .setStiffness(stiffness));
         mSpring.setStartVelocity(0.01f);
         mProperty = new SpringProperty<T>(name, mSpring);
         mObjectAnimator = ObjectAnimator.ofFloat(object, mProperty, values);
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 6ad69d7..fa4ebaf 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -17,13 +17,16 @@
 package com.android.launcher3.config;
 
 import static androidx.core.util.Preconditions.checkNotNull;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.provider.Settings;
+
 import androidx.annotation.GuardedBy;
 import androidx.annotation.Keep;
 import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.Utilities;
 
 import java.util.ArrayList;
@@ -95,8 +98,9 @@
     public static final TogglableFlag APPLY_CONFIG_AT_RUNTIME = new TogglableFlag(
             "APPLY_CONFIG_AT_RUNTIME", true, "Apply display changes dynamically");
 
-    public static final TogglableFlag ENABLE_TASK_STABILIZER = new TogglableFlag(
-            "ENABLE_TASK_STABILIZER", false, "Stable task list across fast task switches");
+    public static final ToggleableGlobalSettingsFlag ENABLE_TASK_STABILIZER
+            = new ToggleableGlobalSettingsFlag("ENABLE_TASK_STABILIZER", false,
+            "Stable task list across fast task switches");
 
     public static final TogglableFlag QUICKSTEP_SPRINGS = new TogglableFlag("QUICKSTEP_SPRINGS",
             false, "Enable springs for quickstep animations");
@@ -249,11 +253,17 @@
 
         @Override
         void updateStorage(Context context, boolean value) {
+            if (contentResolver == null) {
+                return;
+            }
             Settings.Global.putInt(contentResolver, getKey(), value ? 1 : 0);
         }
 
         @Override
         boolean getFromStorage(Context context, boolean defaultValue) {
+            if (contentResolver == null) {
+                return defaultValue;
+            }
             return Settings.Global.getInt(contentResolver, getKey(), defaultValue ? 1 : 0) == 1;
         }
 
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 1b34598..f2a8da5 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -358,4 +358,17 @@
         waitForLauncherCondition(
                 "Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT);
     }
+
+    protected boolean isInBackground(Launcher launcher) {
+        return !launcher.hasBeenResumed();
+    }
+
+    protected boolean isInState(LauncherState state) {
+        if (!TestHelpers.isInLauncherProcess()) return true;
+        return getFromLauncher(launcher -> launcher.getStateManager().getState() == state);
+    }
+
+    protected int getAllAppsScroll(Launcher launcher) {
+        return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
new file mode 100644
index 0000000..701e33b
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2019 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.ui;
+
+import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.UiDevice;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.tapl.AllApps;
+import com.android.launcher3.tapl.AppIcon;
+import com.android.launcher3.tapl.Widgets;
+import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.views.OptionsPopupView;
+import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.launcher3.widget.WidgetsRecyclerView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
+    private static final String TAG = "TaplTestsAosp";
+
+    private static int sScreenshotCount = 0;
+
+    public static class FailureWatcher extends TestWatcher {
+        private UiDevice mDevice;
+
+        public FailureWatcher(UiDevice device) {
+            this.mDevice = device;
+        }
+
+        private void dumpViewHierarchy() {
+            final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+            try {
+                mDevice.dumpWindowHierarchy(stream);
+                stream.flush();
+                stream.close();
+                for (String line : stream.toString().split("\\r?\\n")) {
+                    Log.e(TAG, line.trim());
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "error dumping XML to logcat", e);
+            }
+        }
+
+        @Override
+        protected void failed(Throwable e, Description description) {
+            if (mDevice == null) return;
+            final String pathname = getInstrumentation().getTargetContext().
+                    getFilesDir().getPath() + "/TaplTestScreenshot" + sScreenshotCount++ + ".png";
+            Log.e(TAG, "Failed test " + description.getMethodName() +
+                    ", screenshot will be saved to " + pathname +
+                    ", track trace is below, UI object dump is further below:\n" +
+                    Log.getStackTraceString(e));
+            dumpViewHierarchy();
+            mDevice.takeScreenshot(new File(pathname));
+        }
+    }
+
+    @Rule
+    public TestWatcher mFailureWatcher = new FailureWatcher(mDevice);
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        clearLauncherData();
+
+        mLauncher.pressHome();
+        waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
+        waitForResumed("Launcher internal state is still Background");
+    }
+
+    // Please don't add negative test cases for methods that fail only after a long wait.
+    public static void expectFail(String message, Runnable action) {
+        boolean failed = false;
+        try {
+            action.run();
+        } catch (AssertionError e) {
+            failed = true;
+        }
+        assertTrue(message, failed);
+    }
+
+    private boolean isWorkspaceScrollable(Launcher launcher) {
+        return launcher.getWorkspace().getPageCount() > 1;
+    }
+
+    private int getCurrentWorkspacePage(Launcher launcher) {
+        return launcher.getWorkspace().getCurrentPage();
+    }
+
+    private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
+        return WidgetsFullSheet.getWidgetsView(launcher);
+    }
+
+    @Test
+    public void testDevicePressMenu() throws Exception {
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        executeOnLauncher(
+                launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                        OptionsPopupView.getOptionsPopup(launcher) != null));
+    }
+
+    public static void runAllAppsTest(AbstractLauncherUiTest test, AllApps allApps) {
+        assertNotNull("allApps parameter is null", allApps);
+
+        assertTrue(
+                "Launcher internal state is not All Apps", test.isInState(LauncherState.ALL_APPS));
+
+        // Test flinging forward and backward.
+        test.executeOnLauncher(launcher -> assertEquals(
+                "All Apps started in already scrolled state", 0, test.getAllAppsScroll(launcher)));
+
+        allApps.flingForward();
+        assertTrue("Launcher internal state is not All Apps",
+                test.isInState(LauncherState.ALL_APPS));
+        final Integer flingForwardY = test.getFromLauncher(
+                launcher -> test.getAllAppsScroll(launcher));
+        test.executeOnLauncher(
+                launcher -> assertTrue("flingForward() didn't scroll App Apps", flingForwardY > 0));
+
+        allApps.flingBackward();
+        assertTrue(
+                "Launcher internal state is not All Apps", test.isInState(LauncherState.ALL_APPS));
+        final Integer flingBackwardY = test.getFromLauncher(
+                launcher -> test.getAllAppsScroll(launcher));
+        test.executeOnLauncher(launcher -> assertTrue("flingBackward() didn't scroll App Apps",
+                flingBackwardY < flingForwardY));
+
+        // Test scrolling down to YouTube.
+        assertNotNull("All apps: can't fine YouTube", allApps.getAppIcon("YouTube"));
+        // Test scrolling up to Camera.
+        assertNotNull("All apps: can't fine Camera", allApps.getAppIcon("Camera"));
+        // Test failing to find a non-existing app.
+        final AllApps allAppsFinal = allApps;
+        expectFail("All apps: could find a non-existing app",
+                () -> allAppsFinal.getAppIcon("NO APP"));
+
+        assertTrue(
+                "Launcher internal state is not All Apps", test.isInState(LauncherState.ALL_APPS));
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testAllAppsFromHome() throws Exception {
+        // Test opening all apps
+        assertNotNull("switchToAllApps() returned null",
+                mLauncher.getWorkspace().switchToAllApps());
+
+        runAllAppsTest(this, mLauncher.getAllApps());
+
+        // Testing pressHome.
+        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+        assertNotNull("pressHome returned null", mLauncher.pressHome());
+        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        assertNotNull("getHome returned null", mLauncher.getWorkspace());
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testWorkspaceSwitchToAllApps() {
+        assertNotNull("switchToAllApps() returned null",
+                mLauncher.getWorkspace().switchToAllApps());
+        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+    }
+
+    @Test
+    public void testWorkspace() throws Exception {
+        final Workspace workspace = mLauncher.getWorkspace();
+
+        // Test that ensureWorkspaceIsScrollable adds a page by dragging an icon there.
+        executeOnLauncher(launcher -> assertFalse("Initial workspace state is scrollable",
+                isWorkspaceScrollable(launcher)));
+        assertNull("Messages app was found on empty workspace",
+                workspace.tryGetWorkspaceAppIcon("Messages"));
+
+        workspace.ensureWorkspaceIsScrollable();
+
+        executeOnLauncher(
+                launcher -> assertEquals("Ensuring workspace scrollable didn't switch to page #1",
+                        1, getCurrentWorkspacePage(launcher)));
+        executeOnLauncher(
+                launcher -> assertTrue("ensureScrollable didn't make workspace scrollable",
+                        isWorkspaceScrollable(launcher)));
+        assertNotNull("ensureScrollable didn't add Messages app",
+                workspace.tryGetWorkspaceAppIcon("Messages"));
+
+        // Test flinging workspace.
+        workspace.flingBackward();
+        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+        executeOnLauncher(
+                launcher -> assertEquals("Flinging back didn't switch workspace to page #0",
+                        0, getCurrentWorkspacePage(launcher)));
+
+        workspace.flingForward();
+        executeOnLauncher(
+                launcher -> assertEquals("Flinging forward didn't switch workspace to page #1",
+                        1, getCurrentWorkspacePage(launcher)));
+        assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
+
+        // Test starting a workspace app.
+        final AppIcon app = workspace.tryGetWorkspaceAppIcon("Messages");
+        assertNotNull("No Messages app in workspace", app);
+        assertNotNull("AppIcon.launch returned null",
+                app.launch(resolveSystemApp(Intent.CATEGORY_APP_MESSAGING)));
+        executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top "
+                        + "one",
+                isInBackground(launcher)));
+    }
+
+    public static void runIconLaunchFromAllAppsTest(AbstractLauncherUiTest test, AllApps allApps) {
+        final AppIcon app = allApps.getAppIcon("Calculator");
+        assertNotNull("AppIcon.launch returned null", app.launch(
+                test.resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR)));
+        test.executeOnLauncher(launcher -> assertTrue(
+                "Launcher activity is the top activity; expecting another activity to be the top "
+                        + "one",
+                test.isInBackground(launcher)));
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testAppIconLaunchFromAllAppsFromHome() throws Exception {
+        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
+
+        runIconLaunchFromAllAppsTest(this, allApps);
+    }
+
+    @Test
+    @PortraitLandscape
+    public void testWidgets() throws Exception {
+        // Test opening widgets.
+        executeOnLauncher(launcher ->
+                assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
+        Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
+        assertNotNull("openAllWidgets() returned null", widgets);
+        widgets = mLauncher.getAllWidgets();
+        assertNotNull("getAllWidgets() returned null", widgets);
+        executeOnLauncher(launcher ->
+                assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
+        executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
+                0, getWidgetsScroll(launcher)));
+
+        // Test flinging widgets.
+        widgets.flingForward();
+        Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
+        executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
+                flingForwardY > 0));
+
+        widgets.flingBackward();
+        executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
+                getWidgetsScroll(launcher) < flingForwardY));
+
+        mLauncher.pressHome();
+        waitForLauncherCondition("Widgets were not closed",
+                launcher -> getWidgetsView(launcher) == null);
+    }
+
+    private int getWidgetsScroll(Launcher launcher) {
+        return getWidgetsView(launcher).getCurrentScrollY();
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 3ffd30c..d39a38e 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.tapl;
 
 import android.graphics.Point;
+import android.view.MotionEvent;
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
@@ -40,9 +41,10 @@
      */
     public AppIconMenu openMenu() {
         final Point iconCenter = mObject.getVisibleCenter();
-        mLauncher.longTap(iconCenter.x, iconCenter.y);
+        mLauncher.sendPointer(MotionEvent.ACTION_DOWN, iconCenter);
         final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
                 "deep_shortcuts_container");
+        mLauncher.sendPointer(MotionEvent.ACTION_UP, iconCenter);
         return new AppIconMenu(mLauncher, deepShortcutsContainer);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 49bd73a..444f3bd 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -21,10 +21,13 @@
 import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.UiAutomation;
+import android.graphics.Point;
 import android.os.Bundle;
 import android.os.Parcelable;
+import android.os.SystemClock;
 import android.provider.Settings;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.Surface;
 import android.view.accessibility.AccessibilityEvent;
 
@@ -403,11 +406,6 @@
         return mDevice;
     }
 
-    void longTap(int x, int y) {
-        mDevice.drag(x, y, x, y, 0);
-    }
-
-
     void swipe(int startX, int startY, int endX, int endY) {
         executeAndWaitForEvent(
                 () -> mDevice.swipe(startX, startY, endX, endY, 60),
@@ -419,4 +417,11 @@
     void waitForIdle() {
         mDevice.waitForIdle();
     }
+
+    void sendPointer(int action, Point point) {
+        final MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(),
+                SystemClock.uptimeMillis(), action, point.x, point.y, 0);
+        mInstrumentation.sendPointerSync(event);
+        event.recycle();
+    }
 }
\ No newline at end of file