Merge "Rewrite long swipe resistance ("pullback") logic" into ub-launcher3-rvc-qpr-dev
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 6f4d34c..5026f36 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -58,6 +58,12 @@
                         FeatureFlags.ENABLE_OVERVIEW_ACTIONS.get());
                 return response;
             }
+
+            case TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED: {
+                response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                        FeatureFlags.ENABLE_OVERVIEW_SHARE.get());
+                return response;
+            }
         }
 
         return super.call(method);
diff --git a/quickstep/src/com/android/launcher3/model/WellbeingModel.java b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
index f42b124..cc7b712 100644
--- a/quickstep/src/com/android/launcher3/model/WellbeingModel.java
+++ b/quickstep/src/com/android/launcher3/model/WellbeingModel.java
@@ -43,9 +43,11 @@
 import android.util.Log;
 
 import androidx.annotation.MainThread;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.RemoteActionShortcut;
 import com.android.launcher3.popup.SystemShortcut;
@@ -57,6 +59,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.function.Consumer;
 
 /**
  * Data model for digital wellbeing status of apps.
@@ -72,6 +75,9 @@
     private static final int MSG_FULL_REFRESH = 3;
 
     // Welbeing contract
+    private static final String PATH_ACTIONS = "actions";
+    private static final String PATH_MINIMAL_DEVICE = "minimal_device";
+    private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config";
     private static final String METHOD_GET_ACTIONS = "get_actions";
     private static final String EXTRA_ACTIONS = "actions";
     private static final String EXTRA_ACTION = "action";
@@ -104,15 +110,22 @@
         mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
             @Override
             public void onChange(boolean selfChange, Uri uri) {
-                // Wellbeing reports that app actions have changed.
                 if (DEBUG || mIsInTest) {
-                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
-                            + "], uri = [" + uri + "]");
+                    Log.d(TAG, "ContentObserver.onChange() called with: selfChange = ["
+                            + selfChange + "], uri = [" + uri + "]");
                 }
                 Preconditions.assertUIThread();
-                updateWellbeingData();
+
+                if (uri.getPath().contains(PATH_ACTIONS)) {
+                    // Wellbeing reports that app actions have changed.
+                    updateWellbeingData();
+                } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) {
+                    // Wellbeing reports that minimal device state or config is changed.
+                    updateLauncherModel();
+                }
             }
         };
+        FeatureFlags.ENABLE_MINIMAL_DEVICE.addChangeListener(mContext, this::updateLauncherModel);
 
         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
             context.registerReceiver(
@@ -146,14 +159,18 @@
     private void restartObserver() {
         final ContentResolver resolver = mContext.getContentResolver();
         resolver.unregisterContentObserver(mContentObserver);
-        Uri actionsUri = apiBuilder().path("actions").build();
+        Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
+        Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build();
         try {
             resolver.registerContentObserver(
                     actionsUri, true /* notifyForDescendants */, mContentObserver);
+            resolver.registerContentObserver(
+                    minimalDeviceUri, true /* notifyForDescendants */, mContentObserver);
         } catch (Exception e) {
             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
             if (mIsInTest) throw new RuntimeException(e);
         }
+
         updateWellbeingData();
     }
 
@@ -191,12 +208,42 @@
         mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
     }
 
+    private void updateLauncherModel() {
+        if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) return;
+
+        // TODO: init Launcher in minimal device / normal mode
+    }
+
     private Uri.Builder apiBuilder() {
         return new Uri.Builder()
                 .scheme(SCHEME_CONTENT)
                 .authority(mWellbeingProviderPkg + ".api");
     }
 
+    /**
+     * Fetch most up-to-date minimal device config.
+     */
+    @WorkerThread
+    private void runWithMinimalDeviceConfigs(Consumer<Bundle> consumer) {
+        if (DEBUG || mIsInTest) {
+            Log.d(TAG, "runWithMinimalDeviceConfigs() called");
+        }
+        Preconditions.assertNonUiThread();
+
+        final Uri contentUri = apiBuilder().build();
+        final Bundle remoteBundle;
+        try (ContentProviderClient client = mContext.getContentResolver()
+                .acquireUnstableContentProviderClient(contentUri)) {
+            remoteBundle = client.call(
+                    METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */);
+            consumer.accept(remoteBundle);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
+            if (mIsInTest) throw new RuntimeException(e);
+        }
+        if (DEBUG || mIsInTest) Log.i(TAG, "runWithMinimalDeviceConfigs(): finished");
+    }
+
     private boolean updateActions(String... packageNames) {
         if (packageNames.length == 0) {
             return true;
diff --git a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
index a5d4568..969fa50 100644
--- a/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
+++ b/quickstep/src/com/android/quickstep/util/MotionPauseDetector.java
@@ -358,18 +358,23 @@
 
             if (count < 3) {
                 // Too few samples
-                if (count == 2) {
-                    int endPos = pointPos - 1;
-                    if (endPos < 0) {
-                        endPos += HISTORY_SIZE;
+                switch (count) {
+                    case 2: {
+                        int endPos = pointPos - 1;
+                        if (endPos < 0) {
+                            endPos += HISTORY_SIZE;
+                        }
+                        float denominator = eventTime - mHistoricTimes[endPos];
+                        if (denominator != 0) {
+                            return (mHistoricPos[pointPos] - mHistoricPos[endPos]) / denominator;
+                        }
                     }
-                    float denominator = eventTime - mHistoricTimes[endPos];
-                    if (denominator != 0) {
-                        return (eventTime - mHistoricPos[endPos]) / denominator;
-
-                    }
+                    // fall through
+                    case 1:
+                        return 0f;
+                    default:
+                        return null;
                 }
-                return null;
             }
 
             float Sxx = sxi2 - sxi * sxi / count;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index bf093fd..ecd4e2b 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.tapl.Background;
 import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
 import com.android.launcher3.tapl.Overview;
+import com.android.launcher3.tapl.OverviewActions;
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.ui.TaplTestsLauncher3;
@@ -68,11 +69,14 @@
         });
     }
 
-    private void startTestApps() throws Exception {
+    public static void startTestApps() throws Exception {
         startAppFast(getAppPackageName());
         startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
         startTestActivity(2);
+    }
 
+    private void startTestAppsWithCheck() throws Exception {
+        startTestApps();
         executeOnLauncher(launcher -> assertTrue(
                 "Launcher activity is the top activity; expecting another activity to be the top "
                         + "one",
@@ -105,7 +109,7 @@
     @Test
     @PortraitLandscape
     public void testOverview() throws Exception {
-        startTestApps();
+        startTestAppsWithCheck();
         // mLauncher.pressHome() also tests an important case of pressing home while in background.
         Overview overview = mLauncher.pressHome().switchToOverview();
         assertTrue("Launcher internal state didn't switch to Overview",
@@ -189,6 +193,22 @@
                         0, getTaskCount(launcher)));
     }
 
+    /**
+     * Smoke test for action buttons: Presses all the buttons and makes sure no crashes occur.
+     */
+    @Test
+    @NavigationModeSwitch
+    @PortraitLandscape
+    public void testOverviewActions() throws Exception {
+        if (mLauncher.getNavigationModel() != NavigationModel.TWO_BUTTON) {
+            startTestAppsWithCheck();
+            OverviewActions actionsView =
+                    mLauncher.pressHome().switchToOverview().getOverviewActions();
+            actionsView.clickAndDismissScreenshot();
+            actionsView.clickAndDismissShare();
+        }
+    }
+
     private int getCurrentOverviewPage(Launcher launcher) {
         return launcher.<RecentsView>getOverviewPanel().getCurrentPage();
     }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 626d9dd..58bff09 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -98,6 +98,7 @@
     public static final String REQUEST_DISABLE_DEBUG_TRACING = "disable-debug-tracing";
 
     public static final String REQUEST_OVERVIEW_ACTIONS_ENABLED = "overview-actions-enabled";
+    public static final String REQUEST_OVERVIEW_SHARE_ENABLED = "overview-share-enabled";
 
     public static boolean sDisableSensorRotation;
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 223ae29..588b6b8 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -27,7 +27,7 @@
 import java.util.List;
 
 /**
- * Common overview pane for both Launcher and fallback recents
+ * Common overview panel for both Launcher and fallback recents
  */
 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
@@ -135,4 +135,19 @@
     public boolean hasTasks() {
         return getTasks().size() > 0;
     }
+
+    /**
+     * Gets Overview Actions.
+     *
+     * @return The Overview Actions
+     */
+    @NonNull
+    public OverviewActions getOverviewActions() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get overview actions")) {
+            verifyActiveContainer();
+            UiObject2 overviewActions = mLauncher.waitForLauncherObject("action_buttons");
+            return new OverviewActions(overviewActions, mLauncher);
+        }
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 94c75b0..85f3234 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -154,6 +154,7 @@
     private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
     public static final int WAIT_TIME_MS = 10000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final String ANDROID_PACKAGE = "android";
 
     private static WeakReference<VisibleContainer> sActiveContainer = new WeakReference<>(null);
 
@@ -926,6 +927,14 @@
         return waitForObjectBySelector(getOverviewObjectSelector(resName));
     }
 
+    @NonNull
+    UiObject2 waitForAndroidObject(String resId) {
+        final UiObject2 object = mDevice.wait(
+                Until.findObject(By.res(ANDROID_PACKAGE, resId)), WAIT_TIME_MS);
+        assertNotNull("Can't find a android object with id: " + resId, object);
+        return object;
+    }
+
     private UiObject2 waitForObjectBySelector(BySelector selector) {
         final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
         assertNotNull("Can't find a view in Launcher, selector: " + selector, object);
@@ -1302,6 +1311,11 @@
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    boolean overviewShareEnabled() {
+        return getTestInfo(TestProtocol.REQUEST_OVERVIEW_SHARE_ENABLED).getBoolean(
+                TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     private void disableSensorRotation() {
         getTestInfo(TestProtocol.REQUEST_MOCK_SENSOR_ROTATION);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewActions.java b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
new file mode 100644
index 0000000..a30a404
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewActions.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2020 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+import com.android.launcher3.testing.TestProtocol;
+
+/**
+ * View containing overview actions
+ */
+public class OverviewActions {
+    private final UiObject2 mOverviewActions;
+    private final LauncherInstrumentation mLauncher;
+
+    OverviewActions(UiObject2 overviewActions, LauncherInstrumentation launcherInstrumentation) {
+        this.mOverviewActions = overviewActions;
+        this.mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Clicks screenshot button and closes screenshot ui.
+     */
+    @NonNull
+    public Overview clickAndDismissScreenshot() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "want to click screenshot button and exit screenshot ui")) {
+            UiObject2 screenshot = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_screenshot");
+            mLauncher.clickLauncherObject(screenshot);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked screenshot button")) {
+                UiObject2 closeScreenshot = mLauncher.waitForSystemUiObject(
+                        "global_screenshot_dismiss_image");
+                if (mLauncher.getNavigationModel()
+                        != LauncherInstrumentation.NavigationModel.THREE_BUTTON) {
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_DOWN_TIS);
+                    mLauncher.expectEvent(TestProtocol.SEQUENCE_TIS,
+                            LauncherInstrumentation.EVENT_TOUCH_UP_TIS);
+                }
+                closeScreenshot.click();
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "dismissed screenshot")) {
+                    return new Overview(mLauncher);
+                }
+            }
+        }
+    }
+
+    /**
+     * Click share button, then drags sharesheet down to remove it.
+     *
+     * Share is currently hidden behind flag, test is kept in case share becomes a default feature.
+     * If share is completely removed then remove this test as well.
+     */
+    @NonNull
+    public Overview clickAndDismissShare() {
+        if (mLauncher.overviewShareEnabled()) {
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+                 LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                         "want to click share button and dismiss sharesheet")) {
+                UiObject2 share = mLauncher.waitForObjectInContainer(mOverviewActions,
+                        "action_share");
+                mLauncher.clickLauncherObject(share);
+                try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                        "clicked share button")) {
+                    mLauncher.waitForAndroidObject("contentPanel");
+                    mLauncher.getDevice().pressBack();
+                    try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                            "dismissed sharesheet")) {
+                        return new Overview(mLauncher);
+                    }
+                }
+            }
+        }
+        return new Overview(mLauncher);
+    }
+
+    /**
+     * Click select button
+     *
+     * @return The select mode buttons that are now shown instead of action buttons.
+     */
+    @NonNull
+    public SelectModeButtons clickSelect() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click select button")) {
+            UiObject2 select = mLauncher.waitForObjectInContainer(mOverviewActions,
+                    "action_select");
+            mLauncher.clickLauncherObject(select);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked select button")) {
+                return getSelectModeButtons();
+            }
+
+        }
+    }
+
+    /**
+     * Gets the Select Mode Buttons.
+     *
+     * @return The Select Mode Buttons.
+     */
+    @NonNull
+    private SelectModeButtons getSelectModeButtons() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get select mode buttons")) {
+            UiObject2 selectModeButtons = mLauncher.waitForLauncherObject("select_mode_buttons");
+            return new SelectModeButtons(selectModeButtons, mLauncher);
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
new file mode 100644
index 0000000..3507418
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SelectModeButtons.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * View containing select mode buttons
+ */
+public class SelectModeButtons {
+    private final UiObject2 mSelectModeButtons;
+    private final LauncherInstrumentation mLauncher;
+
+    SelectModeButtons(UiObject2 selectModeButtons,
+            LauncherInstrumentation launcherInstrumentation) {
+        mSelectModeButtons = selectModeButtons;
+        mLauncher = launcherInstrumentation;
+    }
+
+    /**
+     * Click close button.
+     */
+    @NonNull
+    public Overview clickClose() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click close button")) {
+            UiObject2 close = mLauncher.waitForObjectInContainer(mSelectModeButtons, "close");
+            mLauncher.clickLauncherObject(close);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked close button")) {
+                return new Overview(mLauncher);
+            }
+        }
+    }
+
+    /**
+     * Click feedback button.
+     */
+    @NonNull
+    public Background clickFeedback() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c =
+                     mLauncher.addContextLayer("want to click feedback button")) {
+            UiObject2 feedback = mLauncher.waitForObjectInContainer(mSelectModeButtons, "feedback");
+            mLauncher.clickLauncherObject(feedback);
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "clicked feedback button")) {
+                return new Background(mLauncher);
+            }
+        }
+    }
+}