Merge "Fix two pane widget picker bugs" into main
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 05e1535..851f2b3 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -157,8 +157,8 @@
 import com.android.quickstep.util.WorkspaceRevealAnim;
 import com.android.quickstep.views.FloatingWidgetView;
 import com.android.quickstep.views.RecentsView;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.DelegateLaunchAnimatorController;
+import com.android.systemui.animation.ActivityTransitionAnimator;
+import com.android.systemui.animation.DelegateTransitionAnimatorController;
 import com.android.systemui.animation.LaunchableView;
 import com.android.systemui.animation.RemoteAnimationDelegate;
 import com.android.systemui.shared.system.BlurUtils;
@@ -1838,8 +1838,8 @@
 
             // The CUJ is logged by the click handler, so we don't log it inside the animation
             // library.
-            ActivityLaunchAnimator.Controller controllerDelegate =
-                    ActivityLaunchAnimator.Controller.fromView(viewToUse, null /* cujType */);
+            ActivityTransitionAnimator.Controller controllerDelegate =
+                    ActivityTransitionAnimator.Controller.fromView(viewToUse, null /* cujType */);
 
             if (controllerDelegate == null) {
                 return null;
@@ -1847,15 +1847,15 @@
 
             // This wrapper allows us to override the default value, telling the controller that the
             // current window is below the animating window.
-            ActivityLaunchAnimator.Controller controller =
-                    new DelegateLaunchAnimatorController(controllerDelegate) {
+            ActivityTransitionAnimator.Controller controller =
+                    new DelegateTransitionAnimatorController(controllerDelegate) {
                         @Override
                         public boolean isBelowAnimatingWindow() {
                             return true;
                         }
                     };
 
-            ActivityLaunchAnimator.Callback callback = task -> {
+            ActivityTransitionAnimator.Callback callback = task -> {
                 final int backgroundColor =
                         startingWindowListener.mBackgroundColor == Color.TRANSPARENT
                                 ? launcher.getScrimView().getBackgroundColor()
@@ -1863,15 +1863,17 @@
                 return ColorUtils.setAlphaComponent(backgroundColor, 255);
             };
 
-            ActivityLaunchAnimator.Listener listener = new ActivityLaunchAnimator.Listener() {
-                @Override
-                public void onLaunchAnimationEnd() {
-                    onEndCallback.executeAllAndDestroy();
-                }
-            };
+            ActivityTransitionAnimator.Listener listener =
+                    new ActivityTransitionAnimator.Listener() {
+                        @Override
+                        public void onTransitionAnimationEnd() {
+                            onEndCallback.executeAllAndDestroy();
+                        }
+                    };
 
             return new ContainerAnimationRunner(
-                    new ActivityLaunchAnimator.AnimationDelegate(controller, callback, listener));
+                    new ActivityTransitionAnimator.AnimationDelegate(
+                            controller, callback, listener));
         }
 
         /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 8e4a78f..9f6994a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -304,6 +304,10 @@
         callbacks.addListener(mTaskBarRecentsAnimationListener);
         ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(() ->
                 mTaskBarRecentsAnimationListener.endGestureStateOverride(true));
+
+        ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchCancelledRunnable(() -> {
+            updateStateForUserFinishedToApp(false /* finishedToApp */);
+        });
         return animatorSet;
     }
 
@@ -770,21 +774,29 @@
             mTaskBarRecentsAnimationListener = null;
             ((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);
 
-            // Update the visible state immediately to ensure a seamless handoff
-            boolean launcherVisible = !finishedToApp;
-            updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
-            updateStateForFlag(FLAG_VISIBLE, launcherVisible);
-            applyState();
-
-            TaskbarStashController controller = mControllers.taskbarStashController;
-            if (DEBUG) {
-                Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
-            }
-            controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
-            controller.applyState();
+            updateStateForUserFinishedToApp(finishedToApp);
         }
     }
 
+    /**
+     * Updates the visible state immediately to ensure a seamless handoff.
+     * @param finishedToApp True iff user is in an app.
+     */
+    private void updateStateForUserFinishedToApp(boolean finishedToApp) {
+        // Update the visible state immediately to ensure a seamless handoff
+        boolean launcherVisible = !finishedToApp;
+        updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
+        updateStateForFlag(FLAG_VISIBLE, launcherVisible);
+        applyState();
+
+        TaskbarStashController controller = mControllers.taskbarStashController;
+        if (DEBUG) {
+            Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
+        }
+        controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
+        controller.applyState();
+    }
+
     private static String getStateString(int flags) {
         StringJoiner result = new StringJoiner("|");
         appendFlag(result, flags, FLAG_VISIBLE, "flag_visible");
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 8ff43f0..c2cd11c 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -646,6 +646,18 @@
                         });
                     });
                 }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                    super.onAnimationCancel(animation);
+                    recentsView.onTaskLaunchedInLiveTileModeCancelled();
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    super.onAnimationEnd(animation);
+                    recentsView.setTaskLaunchCancelledRunnable(null);
+                }
             };
         } else {
             AnimatorPlaybackController controller =
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index fecbf08..9884d8d 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -759,6 +759,9 @@
     private RunnableList mSideTaskLaunchCallback;
     @Nullable
     private TaskLaunchListener mTaskLaunchListener;
+    @Nullable
+    private Runnable mOnTaskLaunchCancelledRunnable;
+
 
     // keeps track of the state of the filter for tasks in recents view
     private final RecentsFilterState mFilterState = new RecentsFilterState();
@@ -1195,6 +1198,21 @@
         }
     }
 
+    /**
+     * This is a one-time callback when touching in live tile mode. It's reset to null right
+     * after it's called.
+     */
+    public void setTaskLaunchCancelledRunnable(Runnable onTaskLaunchCancelledRunnable) {
+        mOnTaskLaunchCancelledRunnable = onTaskLaunchCancelledRunnable;
+    }
+
+    public void onTaskLaunchedInLiveTileModeCancelled() {
+        if (mOnTaskLaunchCancelledRunnable != null) {
+            mOnTaskLaunchCancelledRunnable.run();
+            mOnTaskLaunchCancelledRunnable = null;
+        }
+    }
+
     private void executeSideTaskLaunchCallback() {
         if (mSideTaskLaunchCallback != null) {
             mSideTaskLaunchCallback.executeAllAndDestroy();
diff --git a/res/layout/widgets_list_row_header_two_pane.xml b/res/layout/widgets_list_row_header_two_pane.xml
index c0a6ea8..bdb2aed 100644
--- a/res/layout/widgets_list_row_header_two_pane.xml
+++ b/res/layout/widgets_list_row_header_two_pane.xml
@@ -23,6 +23,7 @@
     android:importantForAccessibility="yes"
     android:focusable="true"
     launcher:appIconSize="48dp"
+    launcher:collapsable="false"
     android:descendantFocusability="afterDescendants"
     android:background="@drawable/bg_widgets_header_two_pane" >
 
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 754f0cb..4a0b5e8 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -582,6 +582,7 @@
 
     <declare-styleable name="WidgetsListRowHeader">
         <attr name="appIconSize" format="dimension" />
+        <attr name="collapsable" format="boolean" />
     </declare-styleable>
 
     <attr name="materialColorOnSecondaryFixedVariant" format="color" />
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index b5e7401..3383299 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -52,7 +52,11 @@
     private static final int[] EXPANDED_DRAWABLE_STATE = new int[] {android.R.attr.state_expanded};
 
     private final int mIconSize;
-
+    /**
+     * Indicates if the header is collapsable. For example, when displayed in a two pane layout,
+     * widget apps aren't collapsable.
+    */
+    private final boolean mIsCollapsable;
     @Nullable private HandlerRunnable mIconLoadRequest;
     @Nullable private Drawable mIconDrawable;
     @Nullable private WidgetsListDrawableState mListDrawableState;
@@ -79,6 +83,7 @@
                 R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
         mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
                 grid.iconSizePx);
+        mIsCollapsable = a.getBoolean(R.styleable.WidgetsListRowHeader_collapsable, true);
     }
 
     @Override
@@ -87,32 +92,36 @@
         mAppIcon = findViewById(R.id.app_icon);
         mTitle = findViewById(R.id.app_title);
         mSubtitle = findViewById(R.id.app_subtitle);
-        setAccessibilityDelegate(new AccessibilityDelegate() {
+        // Lists that cannot collapse, don't need EXPAND or COLLAPSE accessibility actions.
+        if (mIsCollapsable) {
+            setAccessibilityDelegate(new AccessibilityDelegate() {
 
-            @Override
-            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
-                if (mIsExpanded) {
-                    info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
-                    info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
-                } else {
-                    info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
-                    info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
+                @Override
+                public void onInitializeAccessibilityNodeInfo(View host,
+                        AccessibilityNodeInfo info) {
+                    if (mIsExpanded) {
+                        info.removeAction(AccessibilityNodeInfo.ACTION_EXPAND);
+                        info.addAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+                    } else {
+                        info.removeAction(AccessibilityNodeInfo.ACTION_COLLAPSE);
+                        info.addAction(AccessibilityNodeInfo.ACTION_EXPAND);
+                    }
+                    super.onInitializeAccessibilityNodeInfo(host, info);
                 }
-                super.onInitializeAccessibilityNodeInfo(host, info);
-            }
 
-            @Override
-            public boolean performAccessibilityAction(View host, int action, Bundle args) {
-                switch (action) {
-                    case AccessibilityNodeInfo.ACTION_EXPAND:
-                    case AccessibilityNodeInfo.ACTION_COLLAPSE:
-                        callOnClick();
-                        return true;
-                    default:
-                        return super.performAccessibilityAction(host, action, args);
+                @Override
+                public boolean performAccessibilityAction(View host, int action, Bundle args) {
+                    switch (action) {
+                        case AccessibilityNodeInfo.ACTION_EXPAND:
+                        case AccessibilityNodeInfo.ACTION_COLLAPSE:
+                            callOnClick();
+                            return true;
+                        default:
+                            return super.performAccessibilityAction(host, action, args);
+                    }
                 }
-            }
-        });
+            });
+        }
     }
 
     /** Sets the expand toggle to expand / collapse. */
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/multivalentTests/src/com/android/launcher3/util/rule/FailureWatcher.java
index 10b428a..7fba33e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/rule/FailureWatcher.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/rule/FailureWatcher.java
@@ -125,8 +125,6 @@
             Log.e(TAG, "Failed to save accessibility hierarchy", ex);
         }
 
-        dumpCommand("logcat -d -s TestRunner", diagFile(description, "FilteredLogcat", "txt"));
-
         // Dump bugreport
         if (!sSavedBugreport) {
             dumpCommand("bugreportz -s", diagFile(description, "Bugreport", "zip"));
diff --git a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index dbafe79..2905d85 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -17,9 +17,12 @@
 
 import android.content.Context
 import android.content.res.Configuration
-import android.content.res.Resources
 import android.graphics.Point
 import android.graphics.Rect
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.IgnoreLimit
+import android.platform.test.rule.LimitDevicesRule
 import android.util.DisplayMetrics
 import android.view.Surface
 import androidx.test.core.app.ApplicationProvider
@@ -32,7 +35,6 @@
 import com.android.launcher3.util.rule.TestStabilityRule
 import com.android.launcher3.util.window.CachedDisplayInfo
 import com.android.launcher3.util.window.WindowManagerProxy
-import com.android.wm.shell.Flags
 import com.google.common.truth.Truth
 import java.io.BufferedReader
 import java.io.File
@@ -52,6 +54,8 @@
  *
  * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
  */
+@AllowedDevices(allowed = [DeviceProduct.CF_PHONE])
+@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
 abstract class AbstractDeviceProfileTest {
     protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context
     protected lateinit var context: SandboxContext
@@ -59,15 +63,11 @@
     private val displayController: DisplayController = mock()
     private val windowManagerProxy: WindowManagerProxy = mock()
     private val launcherPrefs: LauncherPrefs = mock()
-    private val allowLeftRightSplitInPortrait: Boolean = initAllowLeftRightSplitInPortrait()
-    fun initAllowLeftRightSplitInPortrait(): Boolean {
-        val res = Resources.getSystem()
-        val resId = res.getIdentifier("config_leftRightSplitInPortrait", "bool", "android")
-        return Flags.enableLeftRightSplitInPortrait() && resId > 0 && res.getBoolean(resId)
-    }
 
     @Rule @JvmField val testStabilityRule = TestStabilityRule()
 
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+
     class DeviceSpec(
         val naturalSize: Pair<Int, Int>,
         var densityDpi: Int,
@@ -311,22 +311,6 @@
     protected fun assertDump(dp: DeviceProfile, folderName: String, filename: String) {
         val dump = dump(context!!, dp, "${folderName}_$filename.txt")
         var expected = readDumpFromAssets(testContext, "$folderName/$filename.txt")
-
-        // TODO(b/315230497): We don't currently have device-specific device profile dumps, so just
-        //  update the result before we do the comparison
-        if (allowLeftRightSplitInPortrait) {
-            val isLeftRightSplitInPortrait =
-                when {
-                    allowLeftRightSplitInPortrait && dp.isTablet -> !dp.isLandscape
-                    else -> dp.isLandscape
-                }
-            expected =
-                expected.replace(
-                    Regex("isLeftRightSplit:\\w+"),
-                    "isLeftRightSplit:$isLeftRightSplitInPortrait"
-                )
-        }
-
         Truth.assertThat(dump).isEqualTo(expected)
     }
 
diff --git a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index 30b5663..251a401 100644
--- a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -18,6 +18,10 @@
 import android.content.Context
 import android.graphics.PointF
 import android.graphics.Rect
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.IgnoreLimit
+import android.platform.test.rule.LimitDevicesRule
 import android.util.SparseArray
 import androidx.test.core.app.ApplicationProvider
 import com.android.launcher3.DeviceProfile.DEFAULT_DIMENSION_PROVIDER
@@ -27,6 +31,7 @@
 import java.io.PrintWriter
 import java.io.StringWriter
 import org.junit.Before
+import org.junit.Rule
 import org.mockito.kotlin.any
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
@@ -37,6 +42,8 @@
  *
  * For an implementation that creates InvariantDeviceProfile, use [AbstractDeviceProfileTest]
  */
+@AllowedDevices(allowed = [DeviceProduct.CF_PHONE])
+@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
 abstract class FakeInvariantDeviceProfileTest {
 
     protected var context: Context? = null
@@ -49,6 +56,8 @@
     protected var isGestureMode: Boolean = true
     protected var isTransientTaskbar: Boolean = true
 
+    @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+
     @Before
     fun setUp() {
         context = ApplicationProvider.getApplicationContext()
diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderAccessibilityTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderAccessibilityTest.java
new file mode 100644
index 0000000..b347f07
--- /dev/null
+++ b/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderAccessibilityTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.widget.picker;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WidgetsListHeaderAccessibilityTest {
+    private Context mContext;
+    private LayoutInflater mLayoutInflater;
+    @Mock
+    private View.OnClickListener mOnClickListener;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = new ActivityContextWrapper(getApplicationContext());
+        mLayoutInflater = LayoutInflater.from(
+                new ContextThemeWrapper(mContext, R.style.WidgetContainerTheme));
+    }
+
+    @Test
+    public void singlePaneCollapsable_hasCustomAccessibilityActions() {
+        WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_header,
+                new FrameLayout(mContext), false);
+
+        assertThat(header.getAccessibilityDelegate()).isNotNull();
+
+        header.setOnClickListener(mOnClickListener);
+        header.getAccessibilityDelegate().performAccessibilityAction(header,
+                AccessibilityNodeInfo.ACTION_EXPAND, null);
+        header.getAccessibilityDelegate().performAccessibilityAction(header,
+                AccessibilityNodeInfo.ACTION_COLLAPSE, null);
+
+        verify(mOnClickListener, times(2)).onClick(header);
+    }
+
+    @Test
+    public void twoPaneNonCollapsable_noCustomAccessibilityDelegate() {
+        WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_header_two_pane,
+                new FrameLayout(mContext), false);
+
+        assertThat(header.getAccessibilityDelegate()).isNull();
+    }
+}