Merge "[E2E] Re-enable stable widget tests." into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 15ac9e3..31a9009 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -323,3 +323,11 @@
description: "Search bar persists at the bottom of the screen across Launcher states"
bug: "346408388"
}
+
+flag {
+ name: "multiline_search_bar"
+ namespace: "launcher"
+ description: "Search bar can wrap to multi-line"
+ bug: "341795751"
+}
+
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index c45c667..7f9d8a3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -27,6 +27,7 @@
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.animation.Interpolator
+import android.window.OnBackInvokedDispatcher
import androidx.core.view.updateLayoutParams
import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
@@ -66,11 +67,14 @@
/** Container where the tooltip's body should be inflated. */
lateinit var content: ViewGroup
private set
+
private lateinit var arrow: View
/** Callback invoked when the tooltip is being closed. */
var onCloseCallback: () -> Unit = {}
private var openCloseAnimator: AnimatorSet? = null
+ /** Used to set whether users can tap outside the current tooltip window to dismiss it */
+ var allowTouchDismissal = true
/** Animates the tooltip into view. */
fun show() {
@@ -134,14 +138,25 @@
override fun isOfType(type: Int): Boolean = type and TYPE_TASKBAR_EDUCATION_DIALOG != 0
override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
- if (ev?.action == ACTION_DOWN && !activityContext.dragLayer.isEventOverView(this, ev)) {
+ if (
+ ev?.action == ACTION_DOWN &&
+ !activityContext.dragLayer.isEventOverView(this, ev) &&
+ allowTouchDismissal
+ ) {
close(true)
}
return false
}
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ findOnBackInvokedDispatcher()
+ ?.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this)
+ }
+
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
+ findOnBackInvokedDispatcher()?.unregisterOnBackInvokedCallback(this)
Settings.Secure.putInt(mContext.contentResolver, LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0)
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
index 5cbd5c9..d57c483 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltipController.kt
@@ -86,10 +86,13 @@
!activityContext.isPhoneMode &&
!activityContext.isTinyTaskbar
}
+
private val isOpen: Boolean
get() = tooltip?.isOpen ?: false
+
val isBeforeTooltipFeaturesStep: Boolean
get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES
+
private lateinit var controllers: TaskbarControllers
// Keep track of whether the user has seen the Search Edu
@@ -152,6 +155,7 @@
tooltipStep = TOOLTIP_STEP_NONE
inflateTooltip(R.layout.taskbar_edu_features)
tooltip?.run {
+ allowTouchDismissal = false
val splitscreenAnim = requireViewById<LottieAnimationView>(R.id.splitscreen_animation)
val suggestionsAnim = requireViewById<LottieAnimationView>(R.id.suggestions_animation)
val pinningAnim = requireViewById<LottieAnimationView>(R.id.pinning_animation)
@@ -216,6 +220,7 @@
inflateTooltip(R.layout.taskbar_edu_pinning)
tooltip?.run {
+ allowTouchDismissal = true
requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
.supportLightTheme()
@@ -260,6 +265,7 @@
userHasSeenSearchEdu = true
inflateTooltip(R.layout.taskbar_edu_search)
tooltip?.run {
+ allowTouchDismissal = true
requireViewById<LottieAnimationView>(R.id.search_edu_animation).supportLightTheme()
val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)
showDisclosureText(eduSubtitle)
@@ -332,7 +338,9 @@
}
/** Closes the current [tooltip]. */
- fun hide() = tooltip?.close(true)
+ fun hide() {
+ tooltip?.close(true)
+ }
/** Initializes [tooltip] with content from [contentResId]. */
private fun inflateTooltip(@LayoutRes contentResId: Int) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index b294208..8a62bf8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -648,8 +648,7 @@
}
}
- @VisibleForTesting
- void addTaskbarRootViewToWindow() {
+ private void addTaskbarRootViewToWindow() {
if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) {
mWindowManager.addView(mTaskbarRootLayout,
mTaskbarActivityContext.getWindowLayoutParams());
@@ -657,8 +656,7 @@
}
}
- @VisibleForTesting
- void removeTaskbarRootViewFromWindow() {
+ private void removeTaskbarRootViewFromWindow() {
if (enableTaskbarNoRecreate() && mAddedWindow) {
mWindowManager.removeViewImmediate(mTaskbarRootLayout);
mAddedWindow = false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 07481a2..753237a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -895,16 +895,18 @@
// where the bubble will end up when the animation ends
final float targetX = expandedX + expandedBarShift;
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
- // When we're expanded, we're not stacked so we're not behind the stack
- bv.setBehindStack(false, animate);
+ // When we're expanded, the badge is visible for all bubbles
+ bv.updateBadgeVisibility(/* show= */ true);
+ bv.setDotScale(widthState);
bv.setAlpha(1);
} else {
// If bar is on the right, account for bubble bar expanding and shifting left
final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
final float targetX = collapsedX + collapsedBarShift;
bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
- // If we're not the first bubble we're behind the stack
- bv.setBehindStack(i > 0, animate);
+ // The badge is always visible for the first bubble
+ bv.updateBadgeVisibility(/* show= */ i == 0);
+ bv.setDotScale(widthState);
// If we're fully collapsed, hide all bubbles except for the first 2. If there are
// only 2 bubbles, hide the second bubble as well because it's the overflow.
if (widthState == 0) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 2f92fbb..0e26c54 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -35,8 +35,6 @@
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;
-import java.util.EnumSet;
-
// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
/**
@@ -47,22 +45,6 @@
public static final int DEFAULT_PATH_SIZE = 100;
- /**
- * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
- * another. If any of these flags are set, the dot will not be shown.
- * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
- */
- enum SuppressionFlag {
- // TODO: (b/277815200) implement flyout
- // Suppressed because the flyout is visible - it will morph into the dot via animation.
- FLYOUT_VISIBLE,
- // Suppressed because this bubble is behind others in the collapsed stack.
- BEHIND_STACK,
- }
-
- private final EnumSet<SuppressionFlag> mSuppressionFlags =
- EnumSet.noneOf(SuppressionFlag.class);
-
private final ImageView mBubbleIcon;
private final ImageView mAppIcon;
private final int mBubbleSize;
@@ -230,7 +212,7 @@
}
}
- void updateBadgeVisibility() {
+ void updateBadgeVisibility(boolean show) {
if (mBubble instanceof BubbleBarOverflow) {
// The overflow bubble does not have a badge, so just bail.
return;
@@ -241,39 +223,24 @@
? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
: 0;
mAppIcon.setTranslationX(translationX);
- mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
- }
-
- /** Sets whether this bubble is in the stack & not the first bubble. **/
- void setBehindStack(boolean behindStack, boolean animate) {
- if (behindStack) {
- mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
- } else {
- mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
- }
- updateDotVisibility(animate);
- updateBadgeVisibility();
- }
-
- /** Whether this bubble is in the stack & not the first bubble. **/
- boolean isBehindStack() {
- return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
+ mAppIcon.setVisibility(show ? VISIBLE : GONE);
}
/** Whether the dot indicating unseen content in a bubble should be shown. */
private boolean shouldDrawDot() {
boolean bubbleHasUnseenContent = mBubble != null
&& mBubble instanceof BubbleBarBubble
- && mSuppressionFlags.isEmpty()
&& !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
-
// Always render the dot if it's animating, since it could be animating out. Otherwise, show
// it if the bubble wants to show it, and we aren't suppressing it.
return bubbleHasUnseenContent || mDotIsAnimating;
}
/** How big the dot should be, fraction from 0 to 1. */
- private void setDotScale(float fraction) {
+ void setDotScale(float fraction) {
+ if (!shouldDrawDot()) {
+ return;
+ }
mDotScale = fraction;
invalidate();
}
@@ -283,14 +250,14 @@
*/
private void animateDotScale() {
float toScale = shouldDrawDot() ? 1f : 0f;
- mDotIsAnimating = true;
+ boolean isDotScaleChanging = Float.compare(mDotScale, toScale) != 0;
- // Don't restart the animation if we're already animating to the given value.
- if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
- mDotIsAnimating = false;
+ // Don't restart the animation if we're already animating to the given value or if the dot
+ // scale is not changing
+ if ((mDotIsAnimating && mAnimatingToDotScale == toScale) || !isDotScaleChanging) {
return;
}
-
+ mDotIsAnimating = true;
mAnimatingToDotScale = toScale;
final boolean showDot = toScale > 0f;
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
index bfad697..9ecd935 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -27,9 +27,10 @@
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.notification.NotificationKeyData
-import com.android.launcher3.taskbar.TaskbarUnitTestRule
-import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.android.launcher3.util.PackageUserKey
@@ -43,7 +44,11 @@
class TaskbarAllAppsControllerTest {
@get:Rule
- val taskbarUnitTestRule = TaskbarUnitTestRule(this, getInstrumentation().targetContext)
+ val taskbarUnitTestRule =
+ TaskbarUnitTestRule(
+ this,
+ TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
+ )
@get:Rule val animatorTestRule = AnimatorTestRule(this)
@InjectController lateinit var allAppsController: TaskbarAllAppsController
@@ -65,9 +70,8 @@
}
@Test
- @UiThreadTest
fun testToggle_taskbarRecreated_allAppsReopened() {
- allAppsController.toggle()
+ getInstrumentation().runOnMainSync { allAppsController.toggle() }
taskbarUnitTestRule.recreateTaskbar()
assertThat(allAppsController.isOpen).isTrue()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
index 72bdc16..fae5562 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt
@@ -26,8 +26,9 @@
import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY
import com.android.launcher3.AbstractFloatingView.hasOpenView
import com.android.launcher3.taskbar.TaskbarActivityContext
-import com.android.launcher3.taskbar.TaskbarUnitTestRule
-import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.android.launcher3.views.BaseDragLayer
@@ -42,7 +43,11 @@
class TaskbarOverlayControllerTest {
@get:Rule
- val taskbarUnitTestRule = TaskbarUnitTestRule(this, getInstrumentation().targetContext)
+ val taskbarUnitTestRule =
+ TaskbarUnitTestRule(
+ this,
+ TaskbarWindowSandboxContext.create(getInstrumentation().targetContext),
+ )
@InjectController lateinit var overlayController: TaskbarOverlayController
private val taskbarContext: TaskbarActivityContext
@@ -150,9 +155,10 @@
}
@Test
- @UiThreadTest
fun testRecreateTaskbar_closesWindow() {
- TestOverlayView.show(overlayController.requestWindow())
+ getInstrumentation().runOnMainSync {
+ TestOverlayView.show(overlayController.requestWindow())
+ }
taskbarUnitTestRule.recreateTaskbar()
assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
similarity index 89%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index 3b53cdc..6638736 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode
-import com.android.launcher3.taskbar.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.MainThreadInitializedObject
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
import com.android.launcher3.util.NavigationMode
import org.junit.rules.TestRule
import org.junit.runner.Description
@@ -40,7 +39,7 @@
* Make sure this rule precedes any rules that depend on [DisplayController], or else the instance
* might be inconsistent across the test lifecycle.
*/
-class TaskbarModeRule(private val context: SandboxContext) : TestRule {
+class TaskbarModeRule(private val context: TaskbarWindowSandboxContext) : TestRule {
/** The selected Taskbar mode. */
enum class Mode {
TRANSIENT,
@@ -60,7 +59,7 @@
override fun evaluate() {
val mode = taskbarMode.mode
- context.putObject(
+ context.applicationContext.putObject(
DisplayController.INSTANCE,
object : DisplayController(context) {
override fun getInfo(): Info {
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
similarity index 87%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index 7dfbb9a..f75e542 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarModeRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
@@ -14,17 +14,16 @@
* limitations under the License.
*/
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.InvariantDeviceProfile
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.PINNED
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.THREE_BUTTONS
-import com.android.launcher3.taskbar.TaskbarModeRule.Mode.TRANSIENT
-import com.android.launcher3.taskbar.TaskbarModeRule.TaskbarMode
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
+import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.LauncherMultivalentJUnit
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
import com.android.launcher3.util.NavigationMode
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -34,7 +33,7 @@
@RunWith(LauncherMultivalentJUnit::class)
class TaskbarModeRuleTest {
- private val context = SandboxContext(getInstrumentation().targetContext)
+ private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
@get:Rule val taskbarModeRule = TaskbarModeRule(context)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
similarity index 91%
rename from quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt
rename to quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index bbf738e..74c2390 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.rules
import android.app.Instrumentation
import android.app.PendingIntent
-import android.content.Context
import android.content.IIntentSender
import android.content.Intent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ServiceTestRule
import com.android.launcher3.LauncherAppState
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarManager
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric
@@ -62,7 +63,11 @@
* }
* ```
*/
-class TaskbarUnitTestRule(private val testInstance: Any, private val context: Context) : TestRule {
+class TaskbarUnitTestRule(
+ private val testInstance: Any,
+ private val context: TaskbarWindowSandboxContext,
+) : TestRule {
+
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val serviceTestRule = ServiceTestRule()
@@ -114,18 +119,14 @@
try {
// Replace Launcher Taskbar window with test instance.
instrumentation.runOnMainSync {
- launcherTaskbarManager?.removeTaskbarRootViewFromWindow()
+ launcherTaskbarManager?.destroy()
taskbarManager.onUserUnlocked() // Required to complete initialization.
}
injectControllers()
base.evaluate()
} finally {
- // Revert Taskbar window.
- instrumentation.runOnMainSync {
- taskbarManager.destroy()
- launcherTaskbarManager?.addTaskbarRootViewToWindow()
- }
+ instrumentation.runOnMainSync { taskbarManager.destroy() }
}
}
}
@@ -133,7 +134,7 @@
/** Simulates Taskbar recreation lifecycle. */
fun recreateTaskbar() {
- taskbarManager.recreateTaskbar()
+ instrumentation.runOnMainSync { taskbarManager.recreateTaskbar() }
injectControllers()
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
new file mode 100644
index 0000000..8262e0f
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.taskbar.rules
+
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarKeyguardController
+import com.android.launcher3.taskbar.TaskbarManager
+import com.android.launcher3.taskbar.TaskbarStashController
+import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.Description
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023"])
+class TaskbarUnitTestRuleTest {
+
+ private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+ @Test
+ fun testSetup_taskbarInitialized() {
+ onSetup { assertThat(activityContext).isInstanceOf(TaskbarActivityContext::class.java) }
+ }
+
+ @Test
+ fun testRecreateTaskbar_activityContextChanged() {
+ onSetup {
+ val context1 = activityContext
+ recreateTaskbar()
+ val context2 = activityContext
+ assertThat(context1).isNotSameInstanceAs(context2)
+ }
+ }
+
+ @Test
+ fun testTeardown_taskbarDestroyed() {
+ val testRule = TaskbarUnitTestRule(this, context)
+ testRule.apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+ assertThrows(RuntimeException::class.java) { testRule.activityContext }
+ }
+
+ @Test
+ fun testInjectController_validControllerType_isInjected() {
+ val testClass =
+ object {
+ @InjectController lateinit var controller: TaskbarStashController
+ val isInjected: Boolean
+ get() = ::controller.isInitialized
+ }
+
+ TaskbarUnitTestRule(testClass, context).apply(EMPTY_STATEMENT, DESCRIPTION).evaluate()
+
+ onSetup(TaskbarUnitTestRule(testClass, context)) {
+ assertThat(testClass.isInjected).isTrue()
+ }
+ }
+
+ @Test
+ fun testInjectController_multipleControllers_areInjected() {
+ val testClass =
+ object {
+ @InjectController lateinit var controller1: TaskbarStashController
+ @InjectController lateinit var controller2: TaskbarKeyguardController
+ val areInjected: Boolean
+ get() = ::controller1.isInitialized && ::controller2.isInitialized
+ }
+
+ onSetup(TaskbarUnitTestRule(testClass, context)) {
+ assertThat(testClass.areInjected).isTrue()
+ }
+ }
+
+ @Test
+ fun testInjectController_invalidControllerType_exceptionThrown() {
+ val testClass =
+ object {
+ @InjectController lateinit var manager: TaskbarManager // Not a controller.
+ }
+
+ // We cannot use #assertThrows because we also catch an assumption violated exception when
+ // running #evaluate on devices that do not support Taskbar.
+ val result =
+ try {
+ TaskbarUnitTestRule(testClass, context)
+ .apply(EMPTY_STATEMENT, DESCRIPTION)
+ .evaluate()
+ } catch (e: NoSuchElementException) {
+ e
+ }
+ assertThat(result).isInstanceOf(NoSuchElementException::class.java)
+ }
+
+ @Test
+ fun testInjectController_recreateTaskbar_controllerChanged() {
+ val testClass =
+ object {
+ @InjectController lateinit var controller: TaskbarStashController
+ }
+
+ onSetup(TaskbarUnitTestRule(testClass, context)) {
+ val controller1 = testClass.controller
+ recreateTaskbar()
+ val controller2 = testClass.controller
+ assertThat(controller1).isNotSameInstanceAs(controller2)
+ }
+ }
+
+ /** Executes [runTest] after the [testRule] setup phase completes. */
+ private fun onSetup(
+ testRule: TaskbarUnitTestRule = TaskbarUnitTestRule(this, context),
+ runTest: TaskbarUnitTestRule.() -> Unit,
+ ) {
+ testRule
+ .apply(
+ object : Statement() {
+ override fun evaluate() = runTest(testRule)
+ },
+ DESCRIPTION,
+ )
+ .evaluate()
+ }
+
+ private companion object {
+ private val EMPTY_STATEMENT =
+ object : Statement() {
+ override fun evaluate() = Unit
+ }
+ private val DESCRIPTION =
+ Description.createSuiteDescription(TaskbarUnitTestRuleTest::class.java)
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
new file mode 100644
index 0000000..321e7a9
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.taskbar.rules
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.os.Bundle
+import android.view.Display
+import com.android.launcher3.util.MainThreadInitializedObject
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+
+/**
+ * Sandbox wrapper where [createWindowContext] provides contexts that are still sandboxed within
+ * [application].
+ *
+ * Taskbar can create window contexts, which need to operate under the same sandbox application, but
+ * [Context.getApplicationContext] by default returns the actual application. For this reason,
+ * [SandboxContext] overrides [getApplicationContext] to return itself, which prevents leaving the
+ * sandbox. [SandboxContext] and the real application have different sets of
+ * [MainThreadInitializedObject] instances, so overriding the application prevents the latter set
+ * from leaking into the sandbox. Similarly, this implementation overrides [getApplicationContext]
+ * to return the original sandboxed [application], and it wraps created windowed contexts to
+ * propagate this [application].
+ */
+class TaskbarWindowSandboxContext
+private constructor(private val application: SandboxContext, base: Context) : ContextWrapper(base) {
+
+ override fun createWindowContext(type: Int, options: Bundle?): Context {
+ return TaskbarWindowSandboxContext(application, super.createWindowContext(type, options))
+ }
+
+ override fun createWindowContext(display: Display, type: Int, options: Bundle?): Context {
+ return TaskbarWindowSandboxContext(
+ application,
+ super.createWindowContext(display, type, options),
+ )
+ }
+
+ override fun getApplicationContext() = application
+
+ companion object {
+ /** Creates a [TaskbarWindowSandboxContext] to sandbox [base] for Taskbar tests. */
+ fun create(base: Context): TaskbarWindowSandboxContext {
+ return SandboxContext(base).let { TaskbarWindowSandboxContext(it, it) }
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
new file mode 100644
index 0000000..ad4b4de
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.taskbar.rules
+
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+class TaskbarWindowSandboxContextTest {
+
+ private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
+
+ @Test
+ fun testCreateWindowContext_applicationContextSandboxed() {
+ val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+ assertThat(windowContext.applicationContext).isInstanceOf(SandboxContext::class.java)
+ }
+
+ @Test
+ fun testCreateWindowContext_nested_applicationContextSandboxed() {
+ val windowContext = context.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+ val nestedContext = windowContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null)
+ assertThat(nestedContext.applicationContext).isInstanceOf(SandboxContext::class.java)
+ }
+}