Merge "Removing tracing for a fixed flake" into ub-launcher3-master
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 826a275..5d871c3 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -91,6 +91,17 @@
                   android:taskAffinity="${packageName}.locktask"
                   android:directBootAware="true" />
 
+        <activity
+            android:name="com.android.quickstep.interaction.BackGestureTutorialActivity"
+            android:autoRemoveFromRecents="true"
+            android:excludeFromRecents="true"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
     </application>
 
 </manifest>
diff --git a/quickstep/res/drawable/ic_bulb_outline.xml b/quickstep/res/drawable/ic_bulb_outline.xml
deleted file mode 100644
index ef7bf9a..0000000
--- a/quickstep/res/drawable/ic_bulb_outline.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<!--
-    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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="192dp"
-    android:height="192dp"
-    android:viewportWidth="192"
-    android:viewportHeight="192">
-    <path
-        android:pathData="M96,90.01"
-        android:strokeWidth="2.4297"
-        android:fillColor="#00000000"
-        android:strokeColor="#FFC800"/>
-    <path
-        android:pathData="M153.24,48.09c-0.5,-1.51 -0.99,-2.86 -1.51,-4.12c-3.03,-7.1 -7.26,-13.45 -12.59,-18.88C127.68,13.42 112.4,7 96.1,7c-14,0 -27.66,4.93 -38.45,13.87C47,29.69 39.61,41.99 36.82,55.51c-0.76,3.9 -1.14,7.86 -1.14,11.78c0,16.39 6.36,31.77 17.9,43.3c2.65,2.65 5.59,5.08 8.74,7.23l0.09,24.14v22.03c0,5.33 4.82,10.01 10.32,10.01h0.41h3.85v0c0,6.23 4.53,10.93 10.53,10.93h16.94c6.01,0 10.54,-4.7 10.54,-10.93v0h4.31c5.56,0 10.26,-4.5 10.26,-9.83v-22.01c0.01,-0.06 0.01,-0.13 0.01,-0.2v-23.74c16.75,-11.15 26.73,-30.15 26.73,-50.94C156.31,60.76 155.28,54.3 153.24,48.09zM118.51,111.08l-0.46,0.29l-0.46,0.29v0.55v0.55v4.38v22.77l-14.12,0V95.9h14h2v-2v-8.5v-2h-2H74.53h-2v2v8.5v2h2h14v44.02l-14.13,0l-0.09,-23.21l-0.02,-4.3l0,-0.54l0,-0.54l-0.45,-0.29l-0.45,-0.29l-3.6,-2.36c-2.81,-1.84 -5.41,-3.95 -7.73,-6.28c-9.27,-9.26 -14.38,-21.63 -14.38,-34.82c0,-3.14 0.3,-6.31 0.9,-9.43C53.25,35.35 73.23,19 96.1,19c13.05,0 25.3,5.15 34.48,14.5c4.27,4.34 7.66,9.43 10.08,15.1c0.39,0.96 0.78,2.03 1.18,3.24c1.64,5 2.47,10.2 2.47,15.45c0,17.1 -8.27,32.58 -22.11,41.43L118.51,111.08z"
-        android:fillColor="#4285F4"/>
-</vector>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 378858f..2bc5015 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -69,13 +69,6 @@
     <!-- Content description for a close button. [CHAR LIMIT=NONE] -->
     <string name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
 
-    <!-- Title shown on the notification of Back gesture tutorial. [CHAR LIMIT=30] -->
-    <string name="back_gesture_tutorial_notification_title" translatable="false">Try the new back gesture</string>
-    <!-- Subtitle shown on the notification of Back gesture tutorial. [CHAR LIMIT=60] -->
-    <string name="back_gesture_tutorial_notification_subtitle" translatable="false">Learn how to go back while using your apps</string>
-    <!-- Action text shown on the notification of Back gesture tutorial. [CHAR LIMIT=14] -->
-    <string name="back_gesture_tutorial_notification_action_label" translatable="false">Try it</string>
-
     <!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
     <string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
     <!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
diff --git a/quickstep/res/xml/notification_action.xml b/quickstep/res/xml/notification_action.xml
deleted file mode 100644
index cc3612e..0000000
--- a/quickstep/res/xml/notification_action.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<!--
-    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.
--->
-<alias xmlns:android="http://schemas.android.com/apk/res/android">
-    <intent
-        android:action="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
-</alias>
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java
new file mode 100644
index 0000000..295ab48
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialActivity.java
@@ -0,0 +1,73 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.util.Optional;
+
+/** Shows the Back gesture interactive tutorial in full screen mode. */
+public class BackGestureTutorialActivity extends FragmentActivity {
+
+    Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.back_gesture_tutorial_activity);
+
+        mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
+                TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
+        getSupportFragmentManager().beginTransaction()
+                .add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
+                .commit();
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus) {
+            hideSystemUI();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mFragment.isPresent()) {
+            mFragment.get().onBackPressed();
+        }
+    }
+
+    private void hideSystemUI() {
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
+                        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                        | View.SYSTEM_UI_FLAG_FULLSCREEN);
+        getWindow().setNavigationBarColor(Color.TRANSPARENT);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
new file mode 100644
index 0000000..486d676
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialConfirmController.java
@@ -0,0 +1,64 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.view.View;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+
+import java.util.Optional;
+
+/**
+ * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
+ * {@link TutorialStep#CONFIRM}.
+ */
+final class BackGestureTutorialConfirmController extends BackGestureTutorialController {
+
+    BackGestureTutorialConfirmController(BackGestureTutorialFragment fragment,
+            BackGestureTutorialTypeInfo tutorialTypeInfo) {
+        super(fragment, TutorialStep.CONFIRM, Optional.of(tutorialTypeInfo));
+    }
+
+    @Override
+    Optional<Integer> getTitleStringId() {
+        return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmTitleId());
+    }
+
+    @Override
+    Optional<Integer> getSubtitleStringId() {
+        return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmSubtitleId());
+    }
+
+    @Override
+    Optional<Integer> getActionButtonStringId() {
+        return Optional.of(R.string.back_gesture_tutorial_action_button_label);
+    }
+
+    @Override
+    Optional<Integer> getActionTextButtonStringId() {
+        return Optional.of(R.string.back_gesture_tutorial_action_text_button_label);
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+        hideHandCoachingAnimation();
+        if (button == mActionTextButton) {
+            mFragment.startSystemNavigationSetting();
+        }
+        mFragment.closeTutorial();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
new file mode 100644
index 0000000..3fe91a3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -0,0 +1,165 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.util.Optional;
+
+/**
+ * Defines the behavior of the particular {@link TutorialStep} and implements the transition to it.
+ */
+abstract class BackGestureTutorialController {
+
+    final BackGestureTutorialFragment mFragment;
+    final TutorialStep mTutorialStep;
+    final Optional<BackGestureTutorialTypeInfo> mTutorialTypeInfo;
+    final Button mActionTextButton;
+    final Button mActionButton;
+    final TextView mSubtitleTextView;
+    final ImageButton mCloseButton;
+    final BackGestureTutorialHandAnimation mHandCoachingAnimation;
+    final LinearLayout mTitlesContainer;
+
+    private final TextView mTitleTextView;
+    private final ImageView mHandCoachingView;
+
+    BackGestureTutorialController(
+            BackGestureTutorialFragment fragment,
+            TutorialStep tutorialStep,
+            Optional<BackGestureTutorialTypeInfo> tutorialTypeInfo) {
+        mFragment = fragment;
+        mTutorialStep = tutorialStep;
+        mTutorialTypeInfo = tutorialTypeInfo;
+
+        View rootView = fragment.getRootView();
+        mActionTextButton = rootView.findViewById(
+                R.id.back_gesture_tutorial_fragment_action_text_button);
+        mActionButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_action_button);
+        mSubtitleTextView = rootView.findViewById(
+                R.id.back_gesture_tutorial_fragment_subtitle_view);
+        mTitleTextView = rootView.findViewById(R.id.back_gesture_tutorial_fragment_title_view);
+        mHandCoachingView = rootView.findViewById(
+                R.id.back_gesture_tutorial_fragment_hand_coaching);
+        mHandCoachingAnimation = mFragment.getHandAnimation();
+        mHandCoachingView.bringToFront();
+        mCloseButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button);
+        mTitlesContainer = rootView.findViewById(
+                R.id.back_gesture_tutorial_fragment_titles_container);
+    }
+
+    void transitToController() {
+        updateTitles();
+        updateActionButtons();
+    }
+
+    void hideHandCoachingAnimation() {
+        mHandCoachingAnimation.stop();
+    }
+
+    void onGestureDetected() {
+        hideHandCoachingAnimation();
+
+        if (mTutorialStep == TutorialStep.CONFIRM) {
+            mFragment.closeTutorial();
+            return;
+        }
+
+        if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
+            mFragment.changeController(TutorialStep.ENGAGED,
+                    TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+            return;
+        }
+
+        mFragment.changeController(TutorialStep.CONFIRM);
+    }
+
+    abstract Optional<Integer> getTitleStringId();
+
+    abstract Optional<Integer> getSubtitleStringId();
+
+    abstract Optional<Integer> getActionButtonStringId();
+
+    abstract Optional<Integer> getActionTextButtonStringId();
+
+    abstract void onActionButtonClicked(View button);
+
+    private void updateActionButtons() {
+        updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
+        updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
+    }
+
+    private static void updateButton(Button button, Optional<Integer> stringId,
+            View.OnClickListener listener) {
+        if (!stringId.isPresent()) {
+            button.setVisibility(View.INVISIBLE);
+            return;
+        }
+
+        button.setVisibility(View.VISIBLE);
+        button.setText(stringId.get());
+        button.setOnClickListener(listener);
+    }
+
+    private void updateTitles() {
+        updateTitleView(mTitleTextView, getTitleStringId(),
+                R.style.TextAppearance_BackGestureTutorial_Title);
+        updateTitleView(mSubtitleTextView, getSubtitleStringId(),
+                R.style.TextAppearance_BackGestureTutorial_Subtitle);
+    }
+
+    private static void updateTitleView(TextView textView, Optional<Integer> stringId,
+            int styleId) {
+        if (!stringId.isPresent()) {
+            textView.setVisibility(View.GONE);
+            return;
+        }
+
+        textView.setVisibility(View.VISIBLE);
+        textView.setText(stringId.get());
+        textView.setTextAppearance(styleId);
+    }
+
+    /**
+     * Constructs {@link BackGestureTutorialController} for providing {@link TutorialType} and
+     * {@link TutorialStep}.
+     */
+    static Optional<BackGestureTutorialController> getTutorialController(
+            BackGestureTutorialFragment fragment, TutorialStep tutorialStep,
+            TutorialType tutorialType) {
+        BackGestureTutorialTypeInfo tutorialTypeInfo =
+                BackGestureTutorialTypeInfoProvider.getTutorialTypeInfo(tutorialType);
+        switch (tutorialStep) {
+            case ENGAGED:
+                return Optional.of(
+                        new BackGestureTutorialEngagedController(fragment, tutorialTypeInfo));
+            case CONFIRM:
+                return Optional.of(
+                        new BackGestureTutorialConfirmController(fragment, tutorialTypeInfo));
+            default:
+                throw new AssertionError("Unexpected tutorial step: " + tutorialStep);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
new file mode 100644
index 0000000..c9ee1e2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialEngagedController.java
@@ -0,0 +1,64 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.view.View;
+
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
+
+import java.util.Optional;
+
+/**
+ * An implementation of {@link BackGestureTutorialController} that defines the behavior of the
+ * {@link TutorialStep#ENGAGED}.
+ */
+final class BackGestureTutorialEngagedController extends BackGestureTutorialController {
+
+    BackGestureTutorialEngagedController(
+            BackGestureTutorialFragment fragment, BackGestureTutorialTypeInfo tutorialTypeInfo) {
+        super(fragment, TutorialStep.ENGAGED, Optional.of(tutorialTypeInfo));
+    }
+
+    @Override
+    void transitToController() {
+        super.transitToController();
+        mHandCoachingAnimation.maybeStartLoopedAnimation(mTutorialTypeInfo.get().getTutorialType());
+    }
+
+    @Override
+    Optional<Integer> getTitleStringId() {
+        return Optional.of(mTutorialTypeInfo.get().getTutorialPlaygroundTitleId());
+    }
+
+    @Override
+    Optional<Integer> getSubtitleStringId() {
+        return Optional.of(mTutorialTypeInfo.get().getTutorialEngagedSubtitleId());
+    }
+
+    @Override
+    Optional<Integer> getActionButtonStringId() {
+        return Optional.empty();
+    }
+
+    @Override
+    Optional<Integer> getActionTextButtonStringId() {
+        return Optional.empty();
+    }
+
+    @Override
+    void onActionButtonClicked(View button) {
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
new file mode 100644
index 0000000..54408ce
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -0,0 +1,165 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.launcher3.R;
+
+import java.net.URISyntaxException;
+import java.util.Optional;
+
+/** Shows the Back gesture interactive tutorial. */
+public class BackGestureTutorialFragment extends Fragment {
+
+    private static final String LOG_TAG = "TutorialFragment";
+    private static final String KEY_TUTORIAL_STEP = "tutorialStep";
+    private static final String KEY_TUTORIAL_TYPE = "tutorialType";
+    private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
+            "#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
+                    + ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
+                    + ".:settings:show_fragment=com.android.settings.gestures"
+                    + ".SystemNavigationGestureSettings;end";
+
+    private TutorialStep mTutorialStep;
+    private TutorialType mTutorialType;
+    private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
+    private View mRootView;
+    private BackGestureTutorialHandAnimation mHandCoachingAnimation;
+
+    public static BackGestureTutorialFragment newInstance(
+            TutorialStep tutorialStep, TutorialType tutorialType) {
+        BackGestureTutorialFragment fragment = new BackGestureTutorialFragment();
+        Bundle args = new Bundle();
+        args.putSerializable(KEY_TUTORIAL_STEP, tutorialStep);
+        args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
+        mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
+        mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+    }
+
+    @Override
+    public View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        super.onCreateView(inflater, container, savedInstanceState);
+
+        mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
+                container, /* attachToRoot= */ false);
+        mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
+                .setOnClickListener(this::onCloseButtonClicked);
+        mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
+
+        return mRootView;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        changeController(mTutorialStep, mTutorialType);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mHandCoachingAnimation.stop();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle savedInstanceState) {
+        savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
+        savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
+        super.onSaveInstanceState(savedInstanceState);
+    }
+
+    View getRootView() {
+        return mRootView;
+    }
+
+    BackGestureTutorialHandAnimation getHandAnimation() {
+        return mHandCoachingAnimation;
+    }
+
+    void changeController(TutorialStep tutorialStep) {
+        changeController(tutorialStep, mTutorialType);
+    }
+
+    void changeController(TutorialStep tutorialStep, TutorialType tutorialType) {
+        Optional<BackGestureTutorialController> tutorialController =
+                BackGestureTutorialController.getTutorialController(/* fragment= */ this,
+                        tutorialStep, tutorialType);
+        if (!tutorialController.isPresent()) {
+            return;
+        }
+
+        mTutorialController = tutorialController;
+        mTutorialController.get().transitToController();
+        this.mTutorialStep = mTutorialController.get().mTutorialStep;
+        this.mTutorialType = tutorialType;
+    }
+
+    void onBackPressed() {
+        if (mTutorialController.isPresent()) {
+            mTutorialController.get().onGestureDetected();
+        }
+    }
+
+    void closeTutorial() {
+        getActivity().finish();
+    }
+
+    void startSystemNavigationSetting() {
+        try {
+            startActivityForResult(
+                    Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
+                    /* requestCode= */ 0);
+        } catch (URISyntaxException e) {
+            Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
+        } catch (ActivityNotFoundException e) {
+            Log.e(LOG_TAG, "The launch Activity not found: " + e);
+        }
+    }
+
+    private void onCloseButtonClicked(View button) {
+        closeTutorial();
+    }
+
+    /** Denotes the step of the tutorial. */
+    enum TutorialStep {
+        ENGAGED,
+        CONFIRM,
+    }
+
+    /** Denotes the type of the tutorial. */
+    enum TutorialType {
+        RIGHT_EDGE_BACK_NAVIGATION,
+        LEFT_EDGE_BACK_NAVIGATION,
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
new file mode 100644
index 0000000..d03811d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialHandAnimation.java
@@ -0,0 +1,94 @@
+/*
+ * 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.quickstep.interaction;
+
+import android.content.Context;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+import java.time.Duration;
+
+/** Hand coaching animation. */
+final class BackGestureTutorialHandAnimation {
+
+    // A delay for waiting the Activity fully launches.
+    private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
+
+    private final ImageView mHandCoachingView;
+    private final AnimatedVectorDrawable mGestureAnimation;
+
+    private boolean mIsAnimationPlayed = false;
+
+    BackGestureTutorialHandAnimation(Context context, View rootView) {
+        mHandCoachingView = rootView.findViewById(
+                R.id.back_gesture_tutorial_fragment_hand_coaching);
+        mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context,
+                R.drawable.back_gesture);
+    }
+
+    boolean isRunning() {
+        return mGestureAnimation.isRunning();
+    }
+
+    /**
+     * Starts animation if the playground is launched for the first time.
+     */
+    void maybeStartLoopedAnimation(TutorialType tutorialType) {
+        if (isRunning() || mIsAnimationPlayed) {
+            return;
+        }
+
+        mIsAnimationPlayed = true;
+        clearAnimationCallbacks();
+        mGestureAnimation.registerAnimationCallback(
+                new Animatable2.AnimationCallback() {
+                    @Override
+                    public void onAnimationEnd(Drawable drawable) {
+                        super.onAnimationEnd(drawable);
+                        mGestureAnimation.start();
+                    }
+                });
+        start(tutorialType);
+    }
+
+    private void start(TutorialType tutorialType) {
+        // Because the gesture animation has only the right side form.
+        // The left side form of the gesture animation is made from flipping the View.
+        float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
+        mHandCoachingView.setRotationY(rotationY);
+        mHandCoachingView.setImageDrawable(mGestureAnimation);
+        mHandCoachingView.postDelayed(() -> mGestureAnimation.start(),
+                ANIMATION_START_DELAY.toMillis());
+    }
+
+    private void clearAnimationCallbacks() {
+        mGestureAnimation.clearAnimationCallbacks();
+    }
+
+    void stop() {
+        mIsAnimationPlayed = false;
+        clearAnimationCallbacks();
+        mGestureAnimation.stop();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
new file mode 100644
index 0000000..ac8443d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfo.java
@@ -0,0 +1,109 @@
+/*
+ * 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.quickstep.interaction;
+
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+/** Defines the UI element identifiers for the particular {@link TutorialType}. */
+final class BackGestureTutorialTypeInfo {
+
+    private final TutorialType mTutorialType;
+    private final int mTutorialPlaygroundTitleId;
+    private final int mTutorialEngagedSubtitleId;
+    private final int mTutorialConfirmTitleId;
+    private final int mTutorialConfirmSubtitleId;
+
+    TutorialType getTutorialType() {
+        return mTutorialType;
+    }
+
+    int getTutorialPlaygroundTitleId() {
+        return mTutorialPlaygroundTitleId;
+    }
+
+    int getTutorialEngagedSubtitleId() {
+        return mTutorialEngagedSubtitleId;
+    }
+
+    int getTutorialConfirmTitleId() {
+        return mTutorialConfirmTitleId;
+    }
+
+    int getTutorialConfirmSubtitleId() {
+        return mTutorialConfirmSubtitleId;
+    }
+
+    static Builder builder() {
+        return new Builder();
+    }
+
+    private BackGestureTutorialTypeInfo(
+            TutorialType tutorialType,
+            int tutorialPlaygroundTitleId,
+            int tutorialEngagedSubtitleId,
+            int tutorialConfirmTitleId,
+            int tutorialConfirmSubtitleId) {
+        mTutorialType = tutorialType;
+        mTutorialPlaygroundTitleId = tutorialPlaygroundTitleId;
+        mTutorialEngagedSubtitleId = tutorialEngagedSubtitleId;
+        mTutorialConfirmTitleId = tutorialConfirmTitleId;
+        mTutorialConfirmSubtitleId = tutorialConfirmSubtitleId;
+    }
+
+    /** Builder for producing {@link BackGestureTutorialTypeInfo} objects. */
+    static class Builder {
+
+        private TutorialType mTutorialType;
+        private Integer mTutorialPlaygroundTitleId;
+        private Integer mTutorialEngagedSubtitleId;
+        private Integer mTutorialConfirmTitleId;
+        private Integer mTutorialConfirmSubtitleId;
+
+        Builder setTutorialType(TutorialType tutorialType) {
+            mTutorialType = tutorialType;
+            return this;
+        }
+
+        Builder setTutorialPlaygroundTitleId(int stringId) {
+            mTutorialPlaygroundTitleId = stringId;
+            return this;
+        }
+
+        Builder setTutorialEngagedSubtitleId(int stringId) {
+            mTutorialEngagedSubtitleId = stringId;
+            return this;
+        }
+
+        Builder setTutorialConfirmTitleId(int stringId) {
+            mTutorialConfirmTitleId = stringId;
+            return this;
+        }
+
+        Builder setTutorialConfirmSubtitleId(int stringId) {
+            mTutorialConfirmSubtitleId = stringId;
+            return this;
+        }
+
+        BackGestureTutorialTypeInfo build() {
+            return new BackGestureTutorialTypeInfo(
+                    mTutorialType,
+                    mTutorialPlaygroundTitleId,
+                    mTutorialEngagedSubtitleId,
+                    mTutorialConfirmTitleId,
+                    mTutorialConfirmSubtitleId);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
new file mode 100644
index 0000000..9575d83
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialTypeInfoProvider.java
@@ -0,0 +1,59 @@
+/*
+ * 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.quickstep.interaction;
+
+import com.android.launcher3.R;
+import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+
+/** Provides instances of {@link BackGestureTutorialTypeInfo} for each {@link TutorialType}. */
+final class BackGestureTutorialTypeInfoProvider {
+
+    private static final BackGestureTutorialTypeInfo RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO =
+            BackGestureTutorialTypeInfo.builder()
+                    .setTutorialType(TutorialType.RIGHT_EDGE_BACK_NAVIGATION)
+                    .setTutorialPlaygroundTitleId(
+                            R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge)
+                    .setTutorialEngagedSubtitleId(
+                            R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge)
+                    .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
+                    .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
+                    .build();
+
+    private static final BackGestureTutorialTypeInfo LEFT_EDGE_BACK_NAV_TUTORIAL_INFO =
+            BackGestureTutorialTypeInfo.builder()
+                    .setTutorialType(TutorialType.LEFT_EDGE_BACK_NAVIGATION)
+                    .setTutorialPlaygroundTitleId(
+                            R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge)
+                    .setTutorialEngagedSubtitleId(
+                            R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge)
+                    .setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
+                    .setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
+                    .build();
+
+    static BackGestureTutorialTypeInfo getTutorialTypeInfo(TutorialType tutorialType) {
+        switch (tutorialType) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                return RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                return LEFT_EDGE_BACK_NAV_TUTORIAL_INFO;
+            default:
+                throw new AssertionError("Unexpected tutorial type: " + tutorialType);
+        }
+    }
+
+    private BackGestureTutorialTypeInfoProvider() {
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 7bbd45d..f322061 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -180,6 +180,14 @@
         icon.mLauncher = launcher;
         icon.mDotRenderer = grid.mDotRendererWorkSpace;
         icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
+
+        // Keep the notification dot up to date with the sum of all the content's dots.
+        FolderDotInfo folderDotInfo = new FolderDotInfo();
+        for (WorkspaceItemInfo si : folderInfo.contents) {
+            folderDotInfo.addDotInfo(launcher.getDotInfoForItem(si));
+        }
+        icon.setDotInfo(folderDotInfo);
+
         Folder folder = Folder.fromXml(launcher);
         folder.setDragController(launcher.getDragController());
         folder.setFolderIcon(icon);
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index 36ff07e..9987994 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -114,9 +114,7 @@
         }
         try {
             int itemsDeleted = db.delete(Favorites.TABLE_NAME, whereClause.toString(), profileIds);
-            if (itemsDeleted > 0) {
-                FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
-            }
+            FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
         } catch (IllegalArgumentException exception) {
             // b/147114476
             FileLog.e(TAG, new StringBuilder("Failed to execute delete, where clause: '")
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 88d34da..5ba931d 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -47,7 +47,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-
 /**
  * Popup shown on long pressing an empty space in launcher
  */
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index d7096b0..7ea70a1 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,9 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.UNBUNDLED_POSTSUBMIT;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -36,6 +39,7 @@
 import com.android.launcher3.tapl.AppIconMenuItem;
 import com.android.launcher3.tapl.Widgets;
 import com.android.launcher3.tapl.Workspace;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.widget.WidgetsFullSheet;
 import com.android.launcher3.widget.WidgetsRecyclerView;
@@ -100,6 +104,16 @@
         mLauncher.pressHome();
     }
 
+    // b/146432215: remove @Stability after 1/1/2020 if this test doesn't flake
+    @Test
+    @Stability(flavors = LOCAL | UNBUNDLED_POSTSUBMIT)
+    public void testOpenHomeSettingsFromWorkspace() {
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        mLauncher.getWorkspace().getMenu().getMenuItem("Home settings").launch(
+                "com.google.android.apps.nexuslauncher");
+    }
+
     @Test
     @Ignore
     public void testPressHomeOnAllAppsContextMenu() throws Exception {
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 44fc3f7..2da6344 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -16,9 +16,6 @@
 
 package com.android.launcher3.tapl;
 
-import android.graphics.Point;
-import android.os.SystemClock;
-import android.view.MotionEvent;
 import android.widget.TextView;
 
 import androidx.test.uiautomator.By;
@@ -41,14 +38,8 @@
      * Long-clicks the icon to open its menu.
      */
     public AppIconMenu openMenu() {
-        final Point iconCenter = mObject.getVisibleCenter();
-        final long downTime = SystemClock.uptimeMillis();
-        mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, iconCenter);
-        final UiObject2 deepShortcutsContainer = mLauncher.waitForLauncherObject(
-                "deep_shortcuts_container");
-        mLauncher.sendPointer(
-                downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, iconCenter);
-        return new AppIconMenu(mLauncher, deepShortcutsContainer);
+        return new AppIconMenu(mLauncher, mLauncher.clickAndGet(
+                mObject, "deep_shortcuts_container"));
     }
 
     @Override
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b715de0..ce60ef2 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -989,6 +989,16 @@
         return getSystemIntegerRes(context, "config_navBarInteractionMode");
     }
 
+    @NonNull
+    public UiObject2 clickAndGet(@NonNull final UiObject2 target, @NonNull String resName) {
+        final Point targetCenter = target.getVisibleCenter();
+        final long downTime = SystemClock.uptimeMillis();
+        sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter);
+        final UiObject2 result = waitForLauncherObject(resName);
+        sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter);
+        return result;
+    }
+
     private static int getSystemIntegerRes(Context context, String resName) {
         Resources res = context.getResources();
         int resId = res.getIdentifier(resName, "integer", "android");
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
new file mode 100644
index 0000000..14cc2a1
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenu.java
@@ -0,0 +1,43 @@
+/*
+ * 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.UiObject2;
+
+public class OptionsPopupMenu {
+
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mDeepShortcutsContainer;
+
+    OptionsPopupMenu(LauncherInstrumentation launcher,
+                UiObject2 deepShortcutsContainer) {
+        mLauncher = launcher;
+        mDeepShortcutsContainer = deepShortcutsContainer;
+    }
+
+    /**
+     * Returns a menu item with a given label. Fails if it doesn't exist.
+     */
+    @NonNull
+    public OptionsPopupMenuItem getMenuItem(@NonNull final String label) {
+        final UiObject2 obj = mLauncher
+                .getObjectsInContainer(mDeepShortcutsContainer, "bubble_text").stream()
+                .filter(menuItem -> label.equals(menuItem.getText())).findFirst().orElseThrow(() ->
+                        new IllegalStateException("Cannot find option with label: " + label));
+        return new OptionsPopupMenuItem(mLauncher, obj);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
new file mode 100644
index 0000000..600d79d
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/OptionsPopupMenuItem.java
@@ -0,0 +1,46 @@
+/*
+ * 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.tapl;
+
+import androidx.annotation.NonNull;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
+
+public class OptionsPopupMenuItem {
+
+    private final LauncherInstrumentation mLauncher;
+    private final UiObject2 mObject;
+
+    OptionsPopupMenuItem(@NonNull LauncherInstrumentation launcher, @NonNull UiObject2 shortcut) {
+        mLauncher = launcher;
+        mObject = shortcut;
+    }
+
+    /**
+     * Clicks the option.
+     */
+    @NonNull
+    public void launch(@NonNull String expectedPackageName) {
+        LauncherInstrumentation.log("OptionsPopupMenuItem before click "
+                + mObject.getVisibleCenter() + " in " + mObject.getVisibleBounds());
+        mObject.click();
+        mLauncher.assertTrue(
+                "App didn't start: " + By.pkg(expectedPackageName),
+                mLauncher.getDevice().wait(Until.hasObject(By.pkg(expectedPackageName)),
+                        LauncherInstrumentation.WAIT_TIME_MS));
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 3299d5d..3b3bbda 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -276,4 +276,15 @@
                 By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout);
         return widget != null ? new Widget(mLauncher, widget) : null;
     }
+
+    /**
+     * Long-clicks the workspace to open menu.
+     */
+    public OptionsPopupMenu getMenu() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to open menu from workspace")) {
+            return new OptionsPopupMenu(mLauncher, mLauncher.waitForLauncherObject(
+                    "deep_shortcuts_container"));
+        }
+    }
 }
\ No newline at end of file