Merge "Use DoubleShadowIconDrawable to draw shadow behind archived cloud icon" into main
diff --git a/Android.bp b/Android.bp
index e358005..ba04bb3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -42,6 +42,24 @@
],
}
+// Main Launcher source for compose, excluding the build config
+filegroup {
+ name: "launcher-compose-enabled-src",
+ srcs: [
+ "compose/facade/enabled/*.kt",
+ "compose/facade/core/*.kt",
+ "compose/features/**/*.kt",
+ ],
+}
+
+filegroup {
+ name: "launcher-compose-disabled-src",
+ srcs: [
+ "compose/facade/core/*.kt",
+ "compose/facade/disabled/*.kt",
+ ],
+}
+
// Source code for quickstep build, on top of launcher-src
filegroup {
name: "launcher-quickstep-src",
@@ -51,6 +69,24 @@
],
}
+// Source code for quickstep build with compose enabled, on top of launcher-src
+filegroup {
+ name: "launcher-quickstep-compose-enabled-src",
+ srcs: [
+ "quickstep/compose/facade/core/*.kt",
+ "quickstep/compose/facade/enabled/*.kt",
+ "quickstep/compose/features/**/*.kt",
+ ],
+}
+
+filegroup {
+ name: "launcher-quickstep-compose-disabled-src",
+ srcs: [
+ "quickstep/compose/facade/core/*.kt",
+ "quickstep/compose/facade/disabled/*.kt",
+ ],
+}
+
// Alternate source when quickstep is not included
filegroup {
name: "launcher-src_no_quickstep",
@@ -74,6 +110,114 @@
srcs: ["proguard.flags"],
}
+// Opt-in configuration for Launcher3 code depending on Jetpack Compose.
+soong_config_module_type {
+ name: "launcher_compose_java_defaults",
+ module_type: "java_defaults",
+ config_namespace: "ANDROID",
+ bool_variables: ["release_enable_compose_in_launcher"],
+ properties: [
+ "srcs",
+ "static_libs",
+ ],
+}
+
+// Opt-in configuration for Launcher Quickstep code depending on Jetpack Compose.
+soong_config_bool_variable {
+ name: "release_enable_compose_in_launcher",
+}
+
+soong_config_module_type {
+ name: "quickstep_compose_java_defaults",
+ module_type: "java_defaults",
+ config_namespace: "ANDROID",
+ bool_variables: ["release_enable_compose_in_launcher"],
+ properties: [
+ "srcs",
+ "static_libs",
+ ],
+}
+
+soong_config_module_type {
+ name: "launcher_compose_tests_java_defaults",
+ module_type: "java_defaults",
+ config_namespace: "ANDROID",
+ bool_variables: ["release_enable_compose_in_launcher"],
+ properties: [
+ "static_libs",
+ ],
+}
+
+launcher_compose_java_defaults {
+ name: "launcher_compose_defaults",
+ soong_config_variables: {
+ release_enable_compose_in_launcher: {
+ srcs: [
+ ":launcher-compose-enabled-src"
+ ],
+
+ // Compose dependencies
+ static_libs: [
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ ],
+
+ // By default, Compose is disabled and we compile the ComposeFacade
+ // in compose/launcher3/facade/disabled/.
+ conditions_default: {
+ srcs: [
+ ":launcher-compose-disabled-src"
+ ],
+ static_libs: [],
+ },
+ },
+ },
+}
+
+quickstep_compose_java_defaults {
+ name: "quickstep_compose_defaults",
+ soong_config_variables: {
+ release_enable_compose_in_launcher: {
+ srcs: [
+ ":launcher-quickstep-compose-enabled-src"
+ ],
+
+ // Compose dependencies
+ static_libs: [
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ ],
+
+ // By default, Compose is disabled and we compile the ComposeFacade
+ // in compose/quickstep/facade/disabled/.
+ conditions_default: {
+ srcs: [
+ ":launcher-quickstep-compose-disabled-src"
+ ],
+ static_libs: [],
+ },
+ },
+ },
+}
+
+launcher_compose_tests_java_defaults {
+ name: "launcher_compose_tests_defaults",
+ soong_config_variables: {
+ release_enable_compose_in_launcher: {
+ // Compose dependencies
+ static_libs: [
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-test-junit4",
+ "androidx.compose.ui_ui-test-manifest",
+ ],
+
+ conditions_default: {
+ static_libs: [],
+ },
+ },
+ },
+}
+
android_library {
name: "launcher-aosp-tapl",
libs: [
@@ -147,6 +291,9 @@
// Library with all the dependencies for building Launcher3
android_library {
name: "Launcher3ResLib",
+ defaults: [
+ "launcher_compose_defaults",
+ ],
srcs: [],
resource_dirs: ["res"],
static_libs: [
@@ -173,6 +320,7 @@
"kotlinx_coroutines",
"com_android_launcher3_flags_lib",
"com_android_wm_shell_flags_lib",
+
],
manifest: "AndroidManifest-common.xml",
sdk_version: "current",
@@ -250,6 +398,10 @@
// Library with all the source code and dependencies for building Launcher Go
android_library {
name: "Launcher3GoLib",
+ defaults: [
+ "launcher_compose_defaults",
+ "quickstep_compose_defaults",
+ ],
srcs: [
":launcher-src",
":launcher-quickstep-src",
@@ -281,6 +433,10 @@
// Library with all the source code and dependencies for building Quickstep
android_library {
name: "Launcher3QuickStepLib",
+ defaults: [
+ "launcher_compose_defaults",
+ "quickstep_compose_defaults"
+ ],
srcs: [
":launcher-src",
":launcher-quickstep-src",
@@ -308,7 +464,6 @@
// Build rule for Quickstep app.
android_app {
name: "Launcher3QuickStep",
-
static_libs: ["Launcher3QuickStepLib"],
optimize: {
proguard_flags_files: [":launcher-proguard-rules"],
@@ -349,7 +504,6 @@
// eventually be merged into a single target
android_app {
name: "Launcher3Go",
-
static_libs: ["Launcher3GoLib"],
resource_dirs: [],
@@ -386,7 +540,6 @@
}
android_app {
name: "Launcher3QuickStepGo",
-
static_libs: ["Launcher3GoLib"],
resource_dirs: [],
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f9327fe..11740ee 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -21,3 +21,13 @@
description: "Enables rewritten version of TaskThumbnailViews in Overview"
bug: "331753115"
}
+
+flag {
+ name: "enable_hover_of_child_elements_in_taskview"
+ namespace: "launcher_overview"
+ description: "Enables child elements to receive hover events in TaskView and respond visually to those hover events."
+ bug: "342594235"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/compose/facade/core/BaseComposeFacade.kt b/compose/facade/core/BaseComposeFacade.kt
new file mode 100644
index 0000000..bc7ba47
--- /dev/null
+++ b/compose/facade/core/BaseComposeFacade.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.compose.core
+
+import android.content.Context
+import android.view.View
+
+interface BaseComposeFacade {
+ fun isComposeAvailable(): Boolean
+
+ fun initComposeView(appContext: Context): View
+}
diff --git a/compose/facade/disabled/ComposeFacade.kt b/compose/facade/disabled/ComposeFacade.kt
new file mode 100644
index 0000000..c1cbfff
--- /dev/null
+++ b/compose/facade/disabled/ComposeFacade.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.compose
+
+import android.content.Context
+import android.view.View
+import com.android.launcher3.compose.core.BaseComposeFacade
+
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = false
+
+ override fun initComposeView(appContext: Context): View {
+ error(
+ "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+ " other function on ComposeFacade."
+ )
+ }
+}
diff --git a/compose/facade/enabled/ComposeFacade.kt b/compose/facade/enabled/ComposeFacade.kt
new file mode 100644
index 0000000..d98a979f
--- /dev/null
+++ b/compose/facade/enabled/ComposeFacade.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.compose
+
+import android.content.Context
+import android.view.View
+import androidx.compose.ui.platform.ComposeView
+import com.android.launcher3.compose.core.BaseComposeFacade
+
+object ComposeFacade : BaseComposeFacade {
+ override fun isComposeAvailable(): Boolean = true
+
+ override fun initComposeView(appContext: Context): View = ComposeView(appContext)
+}
diff --git a/quickstep/compose/facade/core/QuickstepComposeFeatures.kt b/quickstep/compose/facade/core/QuickstepComposeFeatures.kt
new file mode 100644
index 0000000..ca9e5c9
--- /dev/null
+++ b/quickstep/compose/facade/core/QuickstepComposeFeatures.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.quickstep.compose.core
+
+interface QuickstepComposeFeatures
diff --git a/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt b/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt
new file mode 100644
index 0000000..0a4345a
--- /dev/null
+++ b/quickstep/compose/facade/disabled/QuickstepComposeFacade.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.quickstep.compose
+
+import android.content.Context
+import com.android.launcher3.compose.ComposeFacade
+import com.android.launcher3.compose.core.BaseComposeFacade
+import com.android.quickstep.compose.core.QuickstepComposeFeatures
+
+object QuickstepComposeFacade : BaseComposeFacade, QuickstepComposeFeatures {
+
+ override fun isComposeAvailable() = ComposeFacade.isComposeAvailable()
+
+ override fun initComposeView(appContext: Context) = ComposeFacade.initComposeView(appContext)
+}
diff --git a/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt b/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt
new file mode 100644
index 0000000..97cd300
--- /dev/null
+++ b/quickstep/compose/facade/enabled/QuickstepComposeFacade.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.quickstep.compose
+
+import android.content.Context
+import com.android.launcher3.compose.ComposeFacade
+import com.android.launcher3.compose.core.BaseComposeFacade
+import com.android.quickstep.compose.core.QuickstepComposeFeatures
+
+object QuickstepComposeFacade : BaseComposeFacade, QuickstepComposeFeatures {
+ override fun isComposeAvailable() = ComposeFacade.isComposeAvailable()
+
+ override fun initComposeView(appContext: Context) = ComposeFacade.initComposeView(appContext)
+}
diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml
index 797ea45..a11974c 100644
--- a/quickstep/res/layout/split_instructions_view.xml
+++ b/quickstep/res/layout/split_instructions_view.xml
@@ -41,5 +41,6 @@
android:textColor="?androidprv:attr/textColorOnAccent"
android:layout_marginStart="@dimen/split_instructions_start_margin_cancel"
android:text="@string/toast_split_select_app_cancel"
+ android:textStyle="bold"
android:visibility="gone"/>
</com.android.quickstep.views.SplitInstructionsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index ac1a50a..34193d3 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -19,7 +19,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
- android:id="@+id/task"
+ android:id="@+id/task_view_single"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml
index 64aa7e1..8c7090e 100644
--- a/quickstep/res/layout/task_desktop.xml
+++ b/quickstep/res/layout/task_desktop.xml
@@ -19,7 +19,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
- android:id="@+id/task"
+ android:id="@+id/task_view_desktop"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="true"
diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml
index da2b29f..cb4b98f 100644
--- a/quickstep/res/layout/task_grouped.xml
+++ b/quickstep/res/layout/task_grouped.xml
@@ -24,7 +24,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
- android:id="@+id/task"
+ android:id="@+id/task_view_grouped"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 8bcbb33..037a0f6 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -237,7 +237,7 @@
<!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
<string name="toast_split_select_app">Tap another app to use split screen</string>
<string name="toast_contextual_split_select_app">Choose another app to use split screen</string>
- <string name="toast_split_select_app_cancel"><b>Cancel</b></string>
+ <string name="toast_split_select_app_cancel">Cancel</string>
<string name="toast_split_select_cont_desc">Exit split screen selection</string>
<!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
<string name="toast_split_app_unsupported">Choose another app to use split screen</string>
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index ea2adcf..7338485 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -264,11 +264,19 @@
boolean isThreeButtonNav = mContext.isThreeButtonNav();
DeviceProfile deviceProfile = mContext.getDeviceProfile();
Resources resources = mContext.getResources();
- Point p = !mContext.isUserSetupComplete()
- ? new Point(0, mControllers.taskbarActivityContext.getSetupWindowSize())
- : DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
- mContext.isPhoneMode());
- mNavButtonsView.getLayoutParams().height = p.y;
+
+ int setupSize = mControllers.taskbarActivityContext.getSetupWindowSize();
+ Point p = DimensionUtils.getTaskbarPhoneDimensions(deviceProfile, resources,
+ mContext.isPhoneMode());
+ ViewGroup.LayoutParams navButtonsViewLayoutParams = mNavButtonsView.getLayoutParams();
+ navButtonsViewLayoutParams.width = p.x;
+ if (!mContext.isUserSetupComplete()) {
+ // Setup mode in phone mode uses gesture nav.
+ navButtonsViewLayoutParams.height = setupSize;
+ } else {
+ navButtonsViewLayoutParams.height = p.y;
+ }
+ mNavButtonsView.setLayoutParams(navButtonsViewLayoutParams);
mIsImeRenderingNavButtons =
InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 2cb37ff..9d394a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1136,6 +1136,9 @@
* window.
*/
public void setTaskbarWindowFocusable(boolean focusable) {
+ if (isPhoneMode()) {
+ return;
+ }
if (focusable) {
mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
} else {
@@ -1148,7 +1151,7 @@
* Applies forcibly show flag to taskbar window iff transient taskbar is unstashed.
*/
public void applyForciblyShownFlagWhileTransientTaskbarUnstashed(boolean shouldForceShow) {
- if (!DisplayController.isTransientTaskbar(this)) {
+ if (!DisplayController.isTransientTaskbar(this) || isPhoneMode()) {
return;
}
if (shouldForceShow) {
@@ -1691,7 +1694,7 @@
* @param exclude {@code true} then the magnification region computation will omit the window.
*/
public void excludeFromMagnificationRegion(boolean exclude) {
- if (mIsExcludeFromMagnificationRegion == exclude) {
+ if (mIsExcludeFromMagnificationRegion == exclude || isPhoneMode()) {
return;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 20ab32e..e6b3acd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -470,7 +470,8 @@
// We're changing state to home, should close open popups e.g. Taskbar AllApps
handleOpenFloatingViews = true;
}
- if (mLauncherState == LauncherState.OVERVIEW) {
+ if (mLauncherState == LauncherState.OVERVIEW
+ && !mControllers.taskbarActivityContext.isPhoneMode()) {
// Calling to update the insets in TaskbarInsetController#updateInsetsTouchability
mControllers.taskbarActivityContext.notifyUpdateLayoutParams();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 32ca9f2..4d0cad2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -418,6 +418,7 @@
LayoutParams lp = (LayoutParams) getLayoutParams();
lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
setLayoutParams(lp); // triggers a relayout
+ updateBubbleAccessibilityStates();
}
/**
@@ -887,6 +888,33 @@
updateNotificationDotsIfCollapsed();
}
+ /**
+ * Return child views in the order which they are shown on the screen.
+ * <p>
+ * Child views (bubbles) are always ordered based on recency. The most recent bubble is at index
+ * 0.
+ * For example if the child views are (1)(2)(3) then (1) is the most recent bubble and at index
+ * 0.<br>
+ *
+ * How bubbles show up on the screen depends on the bubble bar location. If the bar is on the
+ * left, the most recent bubble is shown on the right. The bubbles from the example above would
+ * be shown as: (3)(2)(1).<br>
+ *
+ * If bubble bar is on the right, then the most recent bubble is on the left. Bubbles from the
+ * example above would be shown as: (1)(2)(3).
+ */
+ private List<View> getChildViewsInOnScreenOrder() {
+ List<View> childViews = new ArrayList<>(getChildCount());
+ for (int i = 0; i < getChildCount(); i++) {
+ childViews.add(getChildAt(i));
+ }
+ if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ // Visually child views are shown in reverse order when bar is on the left
+ return childViews.reversed();
+ }
+ return childViews;
+ }
+
private void updateNotificationDotsIfCollapsed() {
if (isExpanded()) {
return;
@@ -1331,21 +1359,39 @@
}
private void updateBubbleAccessibilityStates() {
- final int childA11y;
if (mIsBarExpanded) {
// Bar is expanded, focus on the bubbles
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
+ // Set up a11y navigation order. Get list of child views in the order they are shown
+ // on screen. And use that to set up navigation so that swiping left focuses the view
+ // on the left and swiping right focuses view on the right.
+ View prevChild = null;
+ for (View childView : getChildViewsInOnScreenOrder()) {
+ childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ childView.setFocusable(true);
+ final View finalPrevChild = prevChild;
+ // Always need to set a new delegate to clear out any previous.
+ childView.setAccessibilityDelegate(new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host,
+ AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (finalPrevChild != null) {
+ info.setTraversalAfter(finalPrevChild);
+ }
+ }
+ });
+ prevChild = childView;
+ }
} else {
// Bar is collapsed, only focus on the bar
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_NO;
- }
- for (int i = 0; i < getChildCount(); i++) {
- getChildAt(i).setImportantForAccessibility(childA11y);
- // Only allowing focusing on bubbles when bar is expanded. Otherwise, in talkback mode,
- // bubbles can be navigates to in collapsed mode.
- getChildAt(i).setFocusable(mIsBarExpanded);
+ for (int i = 0; i < getChildCount(); i++) {
+ View childView = getChildAt(i);
+ childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+ childView.setFocusable(false);
+ }
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2cdc0ce..83123b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -303,7 +303,8 @@
/** Whether a new bubble is animating. */
public boolean isAnimatingNewBubble() {
- return mBarView.isAnimatingNewBubble();
+ return mBarView.isAnimatingNewBubble()
+ || (mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimatingBubble());
}
/** The horizontal margin of the bubble bar from the edge of the screen. */
@@ -575,14 +576,14 @@
}
boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome()
|| !mBubbleStashController.isTransientTaskBar();
- if (persistentTaskbarOrOnHome && !isExpanding && !isExpanded()) {
- mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
+ if (persistentTaskbarOrOnHome && !isExpanded()) {
+ mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
return;
}
// only animate the new bubble if we're in an app, have handle view and not auto expanding
- if (isInApp && !isExpanding && mBubbleStashController.getHasHandleView() && !isExpanded()) {
- mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
+ if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) {
+ mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
}
}
@@ -626,6 +627,10 @@
* from SystemUI.
*/
public void setExpandedFromSysui(boolean isExpanded) {
+ if (isAnimatingNewBubble() && isExpanded) {
+ mBubbleBarViewAnimator.expandedWhileAnimating();
+ return;
+ }
if (!isExpanded) {
mBubbleStashController.stashBubbleBar();
} else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index 52f5a29..8158fe7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -245,6 +245,11 @@
mStashedHandleView.setTranslationY(transY);
}
+ /** Returns the translation of the stashed handle. */
+ public float getTranslationY() {
+ return mStashedHandleView.getTranslationY();
+ }
+
/**
* Used by {@link BubbleStashController} to animate the handle when stashing or un stashing.
*/
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
index 0a0cfd0..b745193 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -43,6 +43,8 @@
private val bubbleBarBounceDistanceInPx =
bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
+ fun hasAnimatingBubble() = animatingBubble != null
+
private companion object {
/** The time to show the flyout. */
const val FLYOUT_DELAY_MS: Long = 2500
@@ -58,8 +60,33 @@
private data class AnimatingBubble(
val bubbleView: BubbleView,
val showAnimation: Runnable,
- val hideAnimation: Runnable
- )
+ val hideAnimation: Runnable,
+ val expand: Boolean,
+ val state: State = State.CREATED
+ ) {
+
+ /**
+ * The state of the animation.
+ *
+ * The animation is initially created but will be scheduled later using the [Scheduler].
+ *
+ * The normal uninterrupted cycle is for the bubble notification to animate in, then be in a
+ * transient state and eventually to animate out.
+ *
+ * However different events, such as touch and external signals, may cause the animation to
+ * end earlier.
+ */
+ enum class State {
+ /** The animation is created but not started yet. */
+ CREATED,
+ /** The bubble notification is animating in. */
+ ANIMATING_IN,
+ /** The bubble notification is now fully showing and waiting to be hidden. */
+ IN,
+ /** The bubble notification is animating out. */
+ ANIMATING_OUT
+ }
+ }
/** An interface for scheduling jobs. */
interface Scheduler {
@@ -97,15 +124,18 @@
)
/** Animates a bubble for the state where the bubble bar is stashed. */
- fun animateBubbleInForStashed(b: BubbleBarBubble) {
+ fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
+ // TODO b/346400677: handle animations for the same bubble interrupting each other
+ if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
// and the second part hides it after a delay.
val showAnimation = buildHandleToBubbleBarAnimation()
- val hideAnimation = buildBubbleBarToHandleAnimation()
- animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+ val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
+ animatingBubble =
+ AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
@@ -125,6 +155,7 @@
* visible which helps avoiding further updates when we re-enter the second part.
*/
private fun buildHandleToBubbleBarAnimation() = Runnable {
+ moveToState(AnimatingBubble.State.ANIMATING_IN)
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.visibility = VISIBLE
@@ -138,9 +169,12 @@
// handle. when the handle becomes invisible and we start animating in the bubble bar,
// the translation y is offset by this value to make the transition from the handle to the
// bar smooth.
- val offset: Float = bubbleStashController.getDiffBetweenHandleAndBarCenters()
- val stashedHandleTranslationY: Float =
+ val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
+ val stashedHandleTranslationYForAnimation =
bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
+ val stashedHandleTranslationY =
+ bubbleStashController.getHandleTranslationY() ?: return@Runnable
+ val translationTracker = TranslationTracker(stashedHandleTranslationY)
// this is the total distance that both the stashed handle and the bubble will be traveling
// at the end of the animation the bubble bar will be positioned in the same place when it
@@ -150,15 +184,14 @@
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
animator.addUpdateListener { handle, values ->
- val ty: Float =
- values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+ val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
when {
- ty >= stashedHandleTranslationY -> {
+ ty >= stashedHandleTranslationYForAnimation -> {
// we're in the first leg of the animation. only animate the handle. the bubble
// bar remains hidden during this part of the animation
// map the path [0, stashedHandleTranslationY] to [0,1]
- val fraction = ty / stashedHandleTranslationY
+ val fraction = ty / stashedHandleTranslationYForAnimation
handle.alpha = 1 - fraction
}
ty >= totalTranslationY -> {
@@ -172,8 +205,8 @@
if (bubbleBarView.alpha != 1f) {
// map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
val fraction =
- (ty - stashedHandleTranslationY) /
- (totalTranslationY - stashedHandleTranslationY)
+ (ty - stashedHandleTranslationYForAnimation) /
+ (totalTranslationY - stashedHandleTranslationYForAnimation)
bubbleBarView.alpha = fraction
bubbleBarView.scaleY =
BUBBLE_ANIMATION_INITIAL_SCALE_Y +
@@ -193,18 +226,16 @@
bubbleStashController.updateTaskbarTouchRegion()
}
}
+ translationTracker.updateTyAndExpandIfNeeded(ty)
}
animator.addEndListener { _, _, _, canceled, _, _, _ ->
// if the show animation was canceled, also cancel the hide animation. this is typically
// canceled in this class, but could potentially be canceled elsewhere.
- if (canceled) {
- val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
- scheduler.cancel(hideAnimation)
- animatingBubble = null
- bubbleBarView.onAnimatingBubbleCompleted()
- bubbleBarView.relativePivotY = 1f
+ if (canceled || animatingBubble?.expand == true) {
+ cancelHideAnimation()
return@addEndListener
}
+ moveToState(AnimatingBubble.State.IN)
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
@@ -227,7 +258,8 @@
*/
private fun buildBubbleBarToHandleAnimation() = Runnable {
if (animatingBubble == null) return@Runnable
- val offset = bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
+ moveToState(AnimatingBubble.State.ANIMATING_OUT)
+ val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
val stashedHandleTranslationY =
bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
// this is the total distance that both the stashed handle and the bar will be traveling
@@ -281,6 +313,8 @@
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
+ // TODO b/346400677: handle animations for the same bubble interrupting each other
+ if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
@@ -300,12 +334,14 @@
bubbleStashController.updateTaskbarTouchRegion()
}
}
- animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+ animatingBubble =
+ AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
private fun buildBubbleBarSpringInAnimation() = Runnable {
+ moveToState(AnimatingBubble.State.ANIMATING_IN)
// prepare the bubble bar for the animation
bubbleBarView.onAnimatingBubbleStarted()
bubbleBarView.translationY = bubbleBarView.height.toFloat()
@@ -314,18 +350,31 @@
bubbleBarView.scaleX = 1f
bubbleBarView.scaleY = 1f
+ val translationTracker = TranslationTracker(bubbleBarView.translationY)
+
val animator = PhysicsAnimator.getInstance(bubbleBarView)
animator.setDefaultSpringConfig(springConfig)
animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
- animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
+ animator.addUpdateListener { _, values ->
+ val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
+ translationTracker.updateTyAndExpandIfNeeded(ty)
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
animator.addEndListener { _, _, _, _, _, _, _ ->
+ if (animatingBubble?.expand == true) {
+ cancelHideAnimation()
+ } else {
+ moveToState(AnimatingBubble.State.IN)
+ }
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
bubbleStashController.updateTaskbarTouchRegion()
}
animator.start()
}
- fun animateBubbleBarForCollapsed(b: BubbleBarBubble) {
+ fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
+ // TODO b/346400677: handle animations for the same bubble interrupting each other
+ if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
val bubbleView = b.view
val animator = PhysicsAnimator.getInstance(bubbleView)
if (animator.isRunning()) animator.cancel()
@@ -336,7 +385,8 @@
bubbleBarView.onAnimatingBubbleCompleted()
bubbleStashController.updateTaskbarTouchRegion()
}
- animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
+ animatingBubble =
+ AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
@@ -347,17 +397,29 @@
* the bubble bar moves back to its initial position with a spring animation.
*/
private fun buildBubbleBarBounceAnimation() = Runnable {
+ moveToState(AnimatingBubble.State.ANIMATING_IN)
bubbleBarView.onAnimatingBubbleStarted()
val ty = bubbleStashController.bubbleBarTranslationY
val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
springBackAnimation.setDefaultSpringConfig(springConfig)
springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
+ springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
+ if (animatingBubble?.expand == true) {
+ bubbleBarView.isExpanded = true
+ cancelHideAnimation()
+ } else {
+ moveToState(AnimatingBubble.State.IN)
+ }
+ }
// animate the bubble bar up and start the spring back down animation when it ends.
ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
.withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
- .withEndAction { springBackAnimation.start() }
+ .withEndAction {
+ if (animatingBubble?.expand == true) bubbleBarView.isExpanded = true
+ springBackAnimation.start()
+ }
.start()
}
@@ -386,6 +448,25 @@
)
}
+ fun expandedWhileAnimating() {
+ val animatingBubble = animatingBubble ?: return
+ this.animatingBubble = animatingBubble.copy(expand = true)
+ // if we're fully in and waiting to hide, cancel the hide animation and clean up
+ if (animatingBubble.state == AnimatingBubble.State.IN) {
+ bubbleBarView.isExpanded = true
+ cancelHideAnimation()
+ }
+ }
+
+ private fun cancelHideAnimation() {
+ val hideAnimation = animatingBubble?.hideAnimation ?: return
+ scheduler.cancel(hideAnimation)
+ animatingBubble = null
+ bubbleBarView.onAnimatingBubbleCompleted()
+ bubbleBarView.relativePivotY = 1f
+ bubbleStashController.showBubbleBarImmediate()
+ }
+
private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
if (this?.isRunning() == true) cancel()
}
@@ -405,4 +486,37 @@
)
return this
}
+
+ private fun moveToState(state: AnimatingBubble.State) {
+ val animatingBubble = this.animatingBubble ?: return
+ this.animatingBubble = animatingBubble.copy(state = state)
+ }
+
+ /**
+ * Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands
+ * as part of the animation, the expansion should start after the bubble bar reaches the peak
+ * position.
+ */
+ private inner class TranslationTracker(initialTy: Float) {
+ private var previousTy = initialTy
+ private var startedExpanding = false
+ private var reachedPeak = false
+
+ fun updateTyAndExpandIfNeeded(ty: Float) {
+ if (!reachedPeak) {
+ // the bubble bar is positioned at the bottom of the screen and moves up using
+ // negative ty values. the peak is reached the first time we see a value that is
+ // greater than the previous.
+ if (ty > previousTy) {
+ reachedPeak = true
+ }
+ }
+ val expand = animatingBubble?.expand ?: false
+ if (reachedPeak && expand && !startedExpanding) {
+ bubbleBarView.isExpanded = true
+ startedExpanding = true
+ }
+ previousTy = ty
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
index 0f43744..48eb7de 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt
@@ -143,6 +143,9 @@
/** Set the translation Y for the stashed handle. */
fun setHandleTranslationY(translationY: Float)
+ /** Returns the translation of the handle. */
+ fun getHandleTranslationY(): Float?
+
/**
* Returns bubble bar Y position according to [isBubblesShowingOnHome] and
* [isBubblesShowingOnOverview] values. Default implementation only analyse
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
index 62fe221..1b65019 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt
@@ -198,6 +198,8 @@
// no op since does not have a handle view
}
+ override fun getHandleTranslationY(): Float? = null
+
private fun updateExpandedState(expand: Boolean) {
if (bubbleBarViewController.isHiddenForNoBubbles) {
// If there are no bubbles the bar is invisible, nothing to do here.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
index 23e009b..1a4b982 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt
@@ -242,6 +242,8 @@
bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY)
}
+ override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY
+
private fun getStashTranslation(): Float {
return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f
}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
index 9ce2277..384945b 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt
@@ -30,6 +30,8 @@
import androidx.core.view.updateLayoutParams
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
+import com.android.launcher3.testing.TestLogging
+import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.TransformingTouchDelegate
@@ -213,7 +215,15 @@
}
override fun needsUpdate(dataChange: Int, flag: Int) =
- if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
+ if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag)
+
+ override fun onIconLoaded(taskContainer: TaskContainer) {
+ // Update contentDescription of snapshotView only, individual task icon is unused.
+ taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
+ }
+
+ // Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
+ override fun onIconUnloaded(taskContainer: TaskContainer) {}
// thumbnailView is laid out differently and is handled in onMeasure
override fun updateThumbnailSize() {}
@@ -228,6 +238,11 @@
override fun launchTaskAnimated(): RunnableList? {
val recentsView = recentsView ?: return null
+ TestLogging.recordEvent(
+ TestProtocol.SEQUENCE_MAIN,
+ "launchDesktopFromRecents",
+ taskIds.contentToString()
+ )
val endCallback = RunnableList()
val desktopController = recentsView.desktopRecentsController
checkNotNull(desktopController) { "recentsController is null" }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index fa9a282..5a6c278 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -135,6 +135,7 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Flags;
import com.android.launcher3.Insettable;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.PagedView;
@@ -553,6 +554,7 @@
// Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
private float mGridProgress = 0;
private float mTaskThumbnailSplashAlpha = 0;
+ private boolean mBorderEnabled = false;
private boolean mShowAsGridLastOnLayout = false;
private final IntSet mTopRowIdSet = new IntSet();
private int mClearAllShortTotalWidthTranslation = 0;
@@ -1525,6 +1527,7 @@
* Enable or disable showing border on hover and focus change on task views
*/
public void setTaskBorderEnabled(boolean enabled) {
+ mBorderEnabled = enabled;
int taskCount = getTaskViewCount();
for (int i = 0; i < taskCount; i++) {
TaskView taskView = requireTaskViewAt(i);
@@ -2044,6 +2047,7 @@
taskView.setFullscreenProgress(mFullscreenProgress);
taskView.setModalness(mTaskModalness);
taskView.setTaskThumbnailSplashAlpha(mTaskThumbnailSplashAlpha);
+ taskView.setBorderEnabled(mBorderEnabled);
}
}
// resetTaskVisuals is called at the end of dismiss animation which could update
@@ -4917,6 +4921,9 @@
mSplitHiddenTaskView.updateSnapshotRadius();
});
} else if (isInitiatingSplitFromTaskView) {
+ if (Flags.enableHoverOfChildElementsInTaskview()) {
+ mSplitHiddenTaskView.setBorderEnabled(false);
+ }
// Splitting from Overview for fullscreen task
createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration,
true /* dismissingForSplitSelection*/);
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index e10d38c..158ae33 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -42,7 +42,11 @@
companion object {
const val TAG = "TaskMenuViewWithArrow"
- fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean {
+ fun showForTask(
+ taskContainer: TaskContainer,
+ alignedOptionIndex: Int = 0,
+ onClosedCallback: Runnable? = null
+ ): Boolean {
val container: RecentsViewContainer =
RecentsViewContainer.containerFromContext(taskContainer.taskView.context)
val taskMenuViewWithArrow =
@@ -52,7 +56,11 @@
false
) as TaskMenuViewWithArrow<*>
- return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex)
+ return taskMenuViewWithArrow.populateAndShowForTask(
+ taskContainer,
+ alignedOptionIndex,
+ onClosedCallback
+ )
}
}
@@ -98,6 +106,7 @@
private var iconView: IconView? = null
private var scrim: View? = null
private val scrimAlpha = 0.8f
+ private var onClosedCallback: Runnable? = null
override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
@@ -141,7 +150,8 @@
private fun populateAndShowForTask(
taskContainer: TaskContainer,
- alignedOptionIndex: Int
+ alignedOptionIndex: Int,
+ onClosedCallback: Runnable?
): Boolean {
if (isAttachedToWindow) {
return false
@@ -150,6 +160,7 @@
taskView = taskContainer.taskView
this.taskContainer = taskContainer
this.alignedOptionIndex = alignedOptionIndex
+ this.onClosedCallback = onClosedCallback
if (!populateMenu()) return false
addScrim()
show()
@@ -252,6 +263,7 @@
super.closeComplete()
popupContainer.removeView(scrim)
popupContainer.removeView(iconView)
+ onClosedCallback?.run()
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index e189d14..176a538 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -48,6 +48,7 @@
import com.android.launcher3.Flags.enableCursorHoverStates
import com.android.launcher3.Flags.enableFocusOutline
import com.android.launcher3.Flags.enableGridOnlyOverview
+import com.android.launcher3.Flags.enableHoverOfChildElementsInTaskview
import com.android.launcher3.Flags.enableOverviewIconMenu
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
import com.android.launcher3.R
@@ -413,6 +414,26 @@
focusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true)
}
+ /**
+ * Used to cache the hover border state so we don't repeatedly call the border animator with
+ * every hover event when the user hasn't crossed the threshold of the [thumbnailBounds].
+ */
+ private var hoverBorderVisible = false
+ set(value) {
+ if (field == value) {
+ return
+ }
+ field = value
+ Log.d(
+ TAG,
+ "${taskIds.contentToString()} - setting border animator visibility to: $field"
+ )
+ hoverBorderAnimator?.setBorderVisibility(visible = field, animated = true)
+ }
+
+ // Used to cache thumbnail bounds to avoid recalculating on every hover move.
+ private var thumbnailBounds = Rect()
+
private var focusTransitionProgress = 1f
set(value) {
field = value
@@ -511,20 +532,28 @@
override fun onHoverEvent(event: MotionEvent): Boolean {
if (borderEnabled) {
when (event.action) {
- MotionEvent.ACTION_HOVER_ENTER ->
- hoverBorderAnimator?.setBorderVisibility(visible = true, animated = true)
- MotionEvent.ACTION_HOVER_EXIT ->
- hoverBorderAnimator?.setBorderVisibility(visible = false, animated = true)
+ MotionEvent.ACTION_HOVER_ENTER -> {
+ hoverBorderVisible =
+ if (enableHoverOfChildElementsInTaskview()) {
+ getThumbnailBounds(thumbnailBounds)
+ event.isWithinThumbnailBounds()
+ } else {
+ true
+ }
+ }
+ MotionEvent.ACTION_HOVER_MOVE ->
+ if (enableHoverOfChildElementsInTaskview())
+ hoverBorderVisible = event.isWithinThumbnailBounds()
+ MotionEvent.ACTION_HOVER_EXIT -> hoverBorderVisible = false
else -> {}
}
}
return super.onHoverEvent(event)
}
- // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
- // task view
- override fun onInterceptHoverEvent(event: MotionEvent) =
- if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean =
+ if (enableHoverOfChildElementsInTaskview()) super.onInterceptHoverEvent(event)
+ else if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event)
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
val recentsView = recentsView ?: return false
@@ -567,6 +596,9 @@
it.right = width
it.bottom = height
}
+ if (enableHoverOfChildElementsInTaskview()) {
+ getThumbnailBounds(thumbnailBounds)
+ }
}
override fun onRecycle() {
@@ -579,6 +611,7 @@
setOverlayEnabled(false)
onTaskListVisibilityChanged(false)
borderEnabled = false
+ hoverBorderVisible = false
taskViewId = UNBOUND_TASK_VIEW_ID
taskContainers.forEach { it.destroy() }
}
@@ -868,18 +901,11 @@
it.task.icon = icon
it.task.titleDescription = contentDescription
it.task.title = title
- setIcon(it.iconView, icon)
- if (enableOverviewIconMenu()) {
- setText(it.iconView, title)
- }
- it.digitalWellBeingToast?.initialize(it.task)
+ onIconLoaded(it)
}
?.also { request -> pendingIconLoadRequests.add(request) }
} else {
- setIcon(it.iconView, null)
- if (enableOverviewIconMenu()) {
- setText(it.iconView, null)
- }
+ onIconUnloaded(it)
}
}
}
@@ -898,6 +924,21 @@
pendingIconLoadRequests.clear()
}
+ protected open fun onIconLoaded(taskContainer: TaskContainer) {
+ setIcon(taskContainer.iconView, taskContainer.task.icon)
+ if (enableOverviewIconMenu()) {
+ setText(taskContainer.iconView, taskContainer.task.title)
+ }
+ taskContainer.digitalWellBeingToast?.initialize(taskContainer.task)
+ }
+
+ protected open fun onIconUnloaded(taskContainer: TaskContainer) {
+ setIcon(taskContainer.iconView, null)
+ if (enableOverviewIconMenu()) {
+ setText(taskContainer.iconView, null)
+ }
+ }
+
protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) {
with(iconView) {
if (icon != null) {
@@ -1119,6 +1160,11 @@
isClickableAsLiveTile = true
return runnableList
}
+ TestLogging.recordEvent(
+ TestProtocol.SEQUENCE_MAIN,
+ "composeRecentsLaunchAnimator",
+ taskIds.contentToString()
+ )
val runnableList = RunnableList()
with(AnimatorSet()) {
TaskViewUtils.composeRecentsLaunchAnimator(
@@ -1225,10 +1271,17 @@
private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean {
val recentsView = recentsView ?: return false
+ if (enableHoverOfChildElementsInTaskview()) {
+ // Disable hover on all TaskView's whilst menu is showing.
+ recentsView.setTaskBorderEnabled(false)
+ }
return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) {
menuContainer.iconView.revealAnim(/* isRevealing= */ true)
TaskMenuView.showForTask(menuContainer) {
menuContainer.iconView.revealAnim(/* isRevealing= */ false)
+ if (enableHoverOfChildElementsInTaskview()) {
+ recentsView.setTaskBorderEnabled(true)
+ }
}
} else if (container.deviceProfile.isTablet) {
val alignedOptionIndex =
@@ -1248,9 +1301,17 @@
} else {
0
}
- TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex)
+ TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) {
+ if (enableHoverOfChildElementsInTaskview()) {
+ recentsView.setTaskBorderEnabled(true)
+ }
+ }
} else {
- TaskMenuView.showForTask(menuContainer)
+ TaskMenuView.showForTask(menuContainer) {
+ if (enableHoverOfChildElementsInTaskview()) {
+ recentsView.setTaskBorderEnabled(true)
+ }
+ }
}
}
@@ -1583,6 +1644,10 @@
override fun close() {}
}
+ private fun MotionEvent.isWithinThumbnailBounds(): Boolean {
+ return thumbnailBounds.contains(x.toInt(), y.toInt())
+ }
+
companion object {
private const val TAG = "TaskView"
const val FLAG_UPDATE_ICON = 1
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
index e583f63..961d4dc 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarEduTooltipControllerTest.kt
@@ -14,11 +14,18 @@
* limitations under the License.
*/
-package com.android.launcher3.taskbar
+package com.android.launcher3.taskbar.test
+import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.Utilities
+import com.android.launcher3.taskbar.TOOLTIP_STEP_FEATURES
+import com.android.launcher3.taskbar.TOOLTIP_STEP_NONE
+import com.android.launcher3.taskbar.TOOLTIP_STEP_PINNING
+import com.android.launcher3.taskbar.TOOLTIP_STEP_SWIPE
+import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarEduTooltipController
import com.android.launcher3.taskbar.rules.TaskbarModeRule
import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.PINNED
import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.THREE_BUTTONS
@@ -35,12 +42,14 @@
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+@Ignore
class TaskbarEduTooltipControllerTest {
private val context =
@@ -48,25 +57,25 @@
InstrumentationRegistry.getInstrumentation().targetContext
)
- @get:Rule
+ @get:Rule(order = 0)
val tooltipStepPreferenceRule =
TaskbarPreferenceRule(
context,
OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP.prefItem,
)
- @get:Rule
+ @get:Rule(order = 1)
val searchEduPreferenceRule =
TaskbarPreferenceRule(
context,
OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN,
)
- @get:Rule val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context)
+ @get:Rule(order = 2) val taskbarPinningPreferenceRule = TaskbarPinningPreferenceRule(context)
- @get:Rule val taskbarModeRule = TaskbarModeRule(context)
+ @get:Rule(order = 3) val taskbarModeRule = TaskbarModeRule(context)
- @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
+ @get:Rule(order = 4) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
@InjectController lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
@@ -77,6 +86,7 @@
@Before
fun setUp() {
+ Log.e("Taskbar", "TaskbarEduTooltipControllerTest test started")
Utilities.disableRunningInTestHarnessForTests()
}
@@ -85,6 +95,7 @@
if (wasInTestHarness) {
Utilities.enableRunningInTestHarnessForTests()
}
+ Log.e("Taskbar", "TaskbarEduTooltipControllerTest test completed")
}
@Test
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 619ce1c..21eb3e0 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -84,7 +84,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleInForStashed(bubble)
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
}
// let the animation start and wait for it to complete
@@ -128,7 +128,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleInForStashed(bubble)
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
}
// let the animation start and wait for it to complete
@@ -171,7 +171,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleInForStashed(bubble)
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
}
// wait for the animation to start
@@ -211,7 +211,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleInForStashed(bubble)
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
}
// let the animation start and wait for it to complete
@@ -252,7 +252,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleInForStashed(bubble)
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
}
// wait for the animation to start
@@ -270,6 +270,123 @@
}
@Test
+ fun animateBubbleInForStashed_autoExpanding() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = true)
+ }
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ assertThat(bubbleBarView.isExpanded).isTrue()
+
+ // verify there is no hide animation
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ verify(bubbleStashController).showBubbleBarImmediate()
+ }
+
+ @Test
+ fun animateBubbleInForStashed_expandedWhileAnimatingIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
+ }
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
+
+ handleAnimator.assertIsRunning()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+ // verify the hide bubble animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // let the animation finish
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ }
+
+ @Test
+ fun animateBubbleInForStashed_expandedWhileFullyIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+
+ val handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble, isExpanding = false)
+ }
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ // wait for the animation to end
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+ // verify the hide bubble animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ assertThat(handle.alpha).isEqualTo(0)
+ assertThat(handle.translationY)
+ .isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
+ verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ }
+
+ @Test
fun animateToInitialState_inApp() {
setUpBubbleBar()
setUpBubbleStashController()
@@ -336,17 +453,11 @@
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
barAnimator.assertIsNotRunning()
- assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
- assertThat(bubbleBarView.alpha).isEqualTo(1)
- assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
-
- assertThat(animatorScheduler.delayedBlock).isNotNull()
- InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
-
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
assertThat(bubbleBarView.alpha).isEqualTo(1)
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
+ assertThat(animatorScheduler.delayedBlock).isNull()
verify(bubbleStashController).showBubbleBarImmediate()
}
@@ -385,6 +496,79 @@
}
@Test
+ fun animateToInitialState_expandedWhileAnimatingIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+ }
+
+ val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true }
+
+ bubbleBarAnimator.assertIsRunning()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+ // verify the hide bubble animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // let the animation finish
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ verify(bubbleStashController).showBubbleBarImmediate()
+ }
+
+ @Test
+ fun animateToInitialState_expandedWhileFullyIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
+ }
+
+ // wait for the animation to start
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+ // verify the hide bubble animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ }
+
+ @Test
fun animateBubbleBarForCollapsed() {
setUpBubbleBar()
setUpBubbleStashController()
@@ -397,7 +581,7 @@
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
- animator.animateBubbleBarForCollapsed(bubble)
+ animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
}
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
@@ -424,6 +608,142 @@
verify(bubbleStashController).showBubbleBarImmediate()
}
+ @Test
+ fun animateBubbleBarForCollapsed_autoExpanding() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleBarForCollapsed(bubble, isExpanding = true)
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ // verify we started animating
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+ // advance the animation handler by the duration of the initial lift
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+
+ // the lift animation is complete; the spring back animation should start now
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ barAnimator.assertIsRunning()
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify there is no hide animation
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ assertThat(bubbleBarView.isExpanded).isTrue()
+ verify(bubbleStashController).showBubbleBarImmediate()
+ }
+
+ @Test
+ fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ // verify we started animating
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+ // advance the animation handler by the duration of the initial lift
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+
+ // the lift animation is complete; the spring back animation should start now
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ barAnimator.assertIsRunning()
+ PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
+
+ // verify there is a pending hide animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // let the animation finish
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ assertThat(bubbleBarView.isExpanded).isTrue()
+ verify(bubbleStashController).showBubbleBarImmediate()
+ }
+
+ @Test
+ fun animateBubbleBarForCollapsed_expandingWhileFullyIn() {
+ setUpBubbleBar()
+ setUpBubbleStashController()
+ whenever(bubbleStashController.bubbleBarTranslationY)
+ .thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+
+ val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
+ }
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ // verify we started animating
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+ // advance the animation handler by the duration of the initial lift
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(250)
+ }
+
+ // the lift animation is complete; the spring back animation should start now
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {}
+ barAnimator.assertIsRunning()
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+ // verify there is a pending hide animation
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.expandedWhileAnimating()
+ }
+
+ // verify that the hide animation was canceled
+ assertThat(animatorScheduler.delayedBlock).isNull()
+
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
+ assertThat(bubbleBarView.isExpanded).isTrue()
+ verify(bubbleStashController).showBubbleBarImmediate()
+ }
+
private fun setUpBubbleBar() {
bubbleBarView = BubbleBarView(context)
InstrumentationRegistry.getInstrumentation().runOnMainSync {
@@ -459,6 +779,14 @@
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
}
+ private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) {
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.scaleX).isEqualTo(1)
+ assertThat(bubbleBarView.scaleY).isEqualTo(1)
+ assertThat(bubbleBarView.translationY).isEqualTo(ty)
+ assertThat(bubbleBarView.isExpanded).isTrue()
+ }
+
private fun <T> PhysicsAnimator<T>.assertIsRunning() {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
assertThat(isRunning()).isTrue()
diff --git a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index 1886ce6..a8f39af 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -31,6 +31,10 @@
static final int STRESS_REPEAT_COUNT = 10;
+ private enum TestCase {
+ TO_HOME, TO_OVERVIEW,
+ }
+
@Override
@Before
public void setUp() throws Exception {
@@ -41,28 +45,55 @@
}
@Test
- @NavigationModeSwitch
+ @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
public void testStressPressHome() {
- for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
- // Destroy Launcher activity.
- closeLauncherActivity();
-
- // The test action.
- mLauncher.goHome();
- }
+ runTest(TestCase.TO_HOME);
}
@Test
- @NavigationModeSwitch
+ @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON)
+ public void testStressSwipeHome() {
+ runTest(TestCase.TO_HOME);
+ }
+
+ @Test
+ @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.THREE_BUTTON)
+ public void testStressPressOverview() {
+ runTest(TestCase.TO_OVERVIEW);
+ }
+
+ @Test
+ @NavigationModeSwitch(mode = NavigationModeSwitchRule.Mode.ZERO_BUTTON)
public void testStressSwipeToOverview() {
+ runTest(TestCase.TO_OVERVIEW);
+ }
+
+ private void runTest(TestCase testCase) {
for (int i = 0; i < STRESS_REPEAT_COUNT; ++i) {
// Destroy Launcher activity.
closeLauncherActivity();
// The test action.
- mLauncher.getLaunchedAppState().switchToOverview();
+ switch (testCase) {
+ case TO_OVERVIEW:
+ mLauncher.getLaunchedAppState().switchToOverview();
+ break;
+ case TO_HOME:
+ mLauncher.goHome();
+ break;
+ default:
+ throw new IllegalStateException("Cannot run test case: " + testCase);
+ }
}
- closeLauncherActivity();
- mLauncher.goHome();
+ switch (testCase) {
+ case TO_OVERVIEW:
+ closeLauncherActivity();
+ mLauncher.goHome();
+ break;
+ case TO_HOME:
+ default:
+ // No-Op
+ break;
+ }
}
}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
new file mode 100644
index 0000000..694a382
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.quickstep
+
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.IgnoreLimit
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.ui.AbstractLauncherUiTest
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape
+import com.android.launcher3.uioverrides.QuickstepLauncher
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+
+/** Test Desktop windowing in Overview. */
+@AllowedDevices(allowed = [DeviceProduct.CF_TABLET, DeviceProduct.TANGORPRO])
+@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
+class TaplTestsOverviewDesktop : AbstractLauncherUiTest<QuickstepLauncher?>() {
+ @Before
+ fun setup() {
+ val overview = mLauncher.goHome().switchToOverview()
+ if (overview.hasTasks()) {
+ overview.dismissAllTasks()
+ }
+ startTestAppsWithCheck()
+ mLauncher.goHome()
+ }
+
+ @Test
+ @PortraitLandscape
+ fun enterDesktopViaOverviewMenu() {
+ // Move last launched TEST_ACTIVITY_2 into Desktop
+ mLauncher.workspace
+ .switchToOverview()
+ .getTestActivityTask(TEST_ACTIVITY_2)
+ .tapMenu()
+ .tapDesktopMenuItem()
+ assertTestAppLaunched(TEST_ACTIVITY_2)
+
+ // Scroll back to TEST_ACTIVITY_1, then move it into Desktop
+ mLauncher
+ .goHome()
+ .switchToOverview()
+ .apply { flingForward() }
+ .getTestActivityTask(TEST_ACTIVITY_1)
+ .tapMenu()
+ .tapDesktopMenuItem()
+ TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+ // Launch static DesktopTaskView
+ val desktop =
+ mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+ TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+
+ // Launch live-tile DesktopTaskView
+ desktop.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
+ TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
+ }
+
+ private fun startTestAppsWithCheck() {
+ TEST_ACTIVITIES.forEach {
+ startTestActivity(it)
+ executeOnLauncher { launcher ->
+ assertWithMessage(
+ "Launcher activity is the top activity; expecting TestActivity$it"
+ )
+ .that(isInLaunchedApp(launcher))
+ .isTrue()
+ }
+ }
+ }
+
+ private fun assertTestAppLaunched(index: Int) {
+ assertWithMessage("TestActivity$index not opened in Desktop")
+ .that(
+ mDevice.wait(
+ Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
+ DEFAULT_UI_TIMEOUT
+ )
+ )
+ .isTrue()
+ }
+
+ companion object {
+ const val TEST_ACTIVITY_1 = 2
+ const val TEST_ACTIVITY_2 = 3
+ val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2)
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
index 512557b..dc1da69 100644
--- a/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaskViewTest.java
@@ -88,18 +88,6 @@
}
@Test
- public void showBorderOnHoverEvent() {
- mTaskView.setBorderEnabled(/* enabled= */ true);
- MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_ENTER, 0.0f, 0.0f, 0);
- mTaskView.onHoverEvent(MotionEvent.obtain(event));
- verify(mHoverAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
- true);
- mTaskView.onFocusChanged(true, 0, new Rect());
- verify(mFocusAnimator, times(1)).setBorderVisibility(/* visible= */ true, /* animated= */
- true);
- }
-
- @Test
public void showBorderOnBorderEnabled() {
presetBorderStatus(/* enabled= */ false);
mTaskView.setBorderEnabled(/* enabled= */ true);
diff --git a/tests/Android.bp b/tests/Android.bp
index e51242f..c99f656 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -71,6 +71,9 @@
// Library with all the dependencies for building quickstep
android_library {
name: "Launcher3TestLib",
+ defaults: [
+ "launcher_compose_tests_defaults",
+ ],
srcs: [],
asset_dirs: ["assets"],
resource_dirs: ["res"],
@@ -112,6 +115,9 @@
android_test {
name: "Launcher3Tests",
+ defaults: [
+ "launcher_compose_tests_defaults",
+ ],
srcs: [
":launcher-tests-src",
":launcher-non-quickstep-tests-src",
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 1e2744c..749a75a 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -43,6 +43,7 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.platform.test.rule.LimitDevicesRule;
import android.system.OsConstants;
import android.util.Log;
@@ -222,6 +223,9 @@
@Rule
public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
+ @Rule
+ public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
+
public static void initialize(AbstractLauncherUiTest test) throws Exception {
test.reinitializeLauncherData();
test.mDevice.pressHome();
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 988aa94..b7ebfcd 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -16,7 +16,7 @@
package com.android.launcher3.tapl;
-import static com.android.launcher3.tapl.BaseOverview.TASK_RES_ID;
+import static com.android.launcher3.tapl.BaseOverview.TASK_SELECTOR;
import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
@@ -117,10 +117,10 @@
// non-tablet overview, snapshots can be on either side of the swiped
// task, but we still check that they become visible after swiping and
// pausing.
- mLauncher.waitForOverviewObject(TASK_RES_ID);
+ mLauncher.waitForObjectBySelector(TASK_SELECTOR);
if (mLauncher.isTablet()) {
List<UiObject2> tasks = mLauncher.getDevice().findObjects(
- mLauncher.getOverviewObjectSelector(TASK_RES_ID));
+ TASK_SELECTOR);
final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
mLauncher.assertTrue(
"All tasks not to the left of the swiped task",
diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 567a8bd..e71b49f 100644
--- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -19,7 +19,9 @@
import static android.view.KeyEvent.KEYCODE_ESCAPE;
import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.LauncherInstrumentation.log;
import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
+import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
import android.graphics.Rect;
@@ -35,9 +37,11 @@
import com.android.launcher3.testing.shared.TestProtocol;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -46,7 +50,9 @@
*/
public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
private static final String TAG = "BaseOverview";
- protected static final String TASK_RES_ID = "task";
+ protected static final BySelector TASK_SELECTOR = By.res(Pattern.compile(
+ getOverviewPackageName()
+ + ":id/(task_view_single|task_view_grouped|task_view_desktop)"));
private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
"Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
@@ -56,10 +62,22 @@
private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
+ private final @Nullable UiObject2 mLiveTileTask;
+
+
BaseOverview(LauncherInstrumentation launcher) {
+ this(launcher, /*launchedFromApp=*/false);
+ }
+
+ BaseOverview(LauncherInstrumentation launcher, boolean launchedFromApp) {
super(launcher);
verifyActiveContainer();
verifyActionsViewVisibility();
+ if (launchedFromApp) {
+ mLiveTileTask = getCurrentTaskUnchecked();
+ } else {
+ mLiveTileTask = null;
+ }
}
@Override
@@ -79,7 +97,7 @@
private void flingForwardImpl() {
try (LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to fling forward in overview")) {
- LauncherInstrumentation.log("Overview.flingForward before fling");
+ log("Overview.flingForward before fling");
final UiObject2 overview = verifyActiveContainer();
final int leftMargin =
mLauncher.getTargetInsets().left + mLauncher.getEdgeSensitivityWidth();
@@ -105,7 +123,7 @@
private void flingBackwardImpl() {
try (LauncherInstrumentation.Closable c =
mLauncher.addContextLayer("want to fling backward in overview")) {
- LauncherInstrumentation.log("Overview.flingBackward before fling");
+ log("Overview.flingBackward before fling");
final UiObject2 overview = verifyActiveContainer();
final int rightMargin =
mLauncher.getTargetInsets().right + mLauncher.getEdgeSensitivityWidth();
@@ -276,37 +294,56 @@
*/
@NonNull
public OverviewTask getCurrentTask() {
+ UiObject2 currentTask = getCurrentTaskUnchecked();
+ mLauncher.assertNotNull("Unable to find a task", currentTask);
+ return new OverviewTask(mLauncher, currentTask, this);
+ }
+
+ @Nullable
+ private UiObject2 getCurrentTaskUnchecked() {
final List<UiObject2> taskViews = getTasks();
- mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
+ if (taskViews.isEmpty()) {
+ return null;
+ }
// The widest, and most top-right task should be the current task
- UiObject2 currentTask = Collections.max(taskViews,
+ return Collections.max(taskViews,
Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
.thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
.thenComparing(Comparator.comparing(
(UiObject2 t) -> t.getVisibleCenter().y).reversed()));
- return new OverviewTask(mLauncher, currentTask, this);
}
- /** Returns an overview task matching TestActivity {@param activityNumber}. */
+ /**
+ * Returns an overview task that contains the specified test activity in its thumbnails.
+ *
+ * @param activityIndex index of TestActivity to match against
+ */
@NonNull
- public OverviewTask getTestActivityTask(int activityNumber) {
+ public OverviewTask getTestActivityTask(int activityIndex) {
+ return getTestActivityTask(Collections.singleton(activityIndex));
+ }
+
+ /**
+ * Returns an overview task that contains all the specified test activities in its thumbnails.
+ *
+ * @param activityNumbers collection of indices of TestActivity to match against
+ */
+ @NonNull
+ public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) {
final List<UiObject2> taskViews = getTasks();
mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
- final String activityName = "TestActivity" + activityNumber;
- UiObject2 task = null;
- for (UiObject2 taskView : taskViews) {
- // TODO(b/239452415): Use equals instead of descEndsWith
- if (taskView.getParent().hasObject(By.descEndsWith(activityName))) {
- task = taskView;
- break;
- }
- }
- mLauncher.assertNotNull(
- "Unable to find a task with " + activityName + " from the task list", task);
+ Optional<UiObject2> task = taskViews.stream().filter(
+ taskView -> activityNumbers.stream().allMatch(activityNumber ->
+ // TODO(b/239452415): Use equals instead of descEndsWith
+ taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber))
+ )).findFirst();
- return new OverviewTask(mLauncher, task, this);
+ mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers
+ + " from the task list", task.isPresent());
+
+ return new OverviewTask(mLauncher, task.get(), this);
}
/**
@@ -328,8 +365,7 @@
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"want to get overview tasks")) {
verifyActiveContainer();
- return mLauncher.getDevice().findObjects(
- mLauncher.getOverviewObjectSelector(TASK_RES_ID));
+ return mLauncher.getDevice().findObjects(TASK_SELECTOR);
}
}
@@ -506,4 +542,10 @@
}
return null;
}
+
+ protected boolean isLiveTile(UiObject2 task) {
+ // UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence
+ // compare only hashCode as a workaround.
+ return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode();
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 200f2ff..b3ad930 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -33,6 +33,7 @@
import android.view.MotionEvent;
import android.view.ViewConfiguration;
+import androidx.annotation.NonNull;
import androidx.test.uiautomator.Condition;
import androidx.test.uiautomator.UiDevice;
@@ -75,6 +76,20 @@
return false;
}
+ @NonNull
+ @Override
+ public BaseOverview switchToOverview() {
+ try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
+ "want to switch from background to overview")) {
+ verifyActiveContainer();
+ goToOverviewUnchecked();
+ return mLauncher.is3PLauncher()
+ ? new BaseOverview(mLauncher, /*launchedFromApp=*/true)
+ : new Overview(mLauncher, /*launchedFromApp=*/true);
+ }
+ }
+
/**
* Returns the taskbar.
*
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 75c1b24..a874062 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1585,7 +1585,7 @@
return objects;
}
- private UiObject2 waitForObjectBySelector(BySelector selector) {
+ UiObject2 waitForObjectBySelector(BySelector selector) {
Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
"LauncherInstrumentation.waitForObjectBySelector");
final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java
index 50c2136..deb27e4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Overview.java
+++ b/tests/tapl/com/android/launcher3/tapl/Overview.java
@@ -22,9 +22,12 @@
* Overview pane.
*/
public class Overview extends BaseOverview {
-
Overview(LauncherInstrumentation launcher) {
- super(launcher);
+ this(launcher, /*launchedFromApp=*/false);
+ }
+
+ Overview(LauncherInstrumentation launcher, boolean launchedFromApp) {
+ super(launcher, launchedFromApp);
}
@Override
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 6f420af..7a8ab49 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -40,16 +40,23 @@
public final class OverviewTask {
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync");
+ static final Pattern TASK_START_EVENT_DESKTOP = Pattern.compile("launchDesktopFromRecents");
+ static final Pattern TASK_START_EVENT_LIVE_TILE = Pattern.compile(
+ "composeRecentsLaunchAnimator");
static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect");
static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks");
private final LauncherInstrumentation mLauncher;
+ @NonNull
private final UiObject2 mTask;
+ private final TaskViewType mType;
private final BaseOverview mOverview;
- OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview) {
+ OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview) {
mLauncher = launcher;
+ mLauncher.assertNotNull("task must not be null", task);
mTask = task;
mOverview = overview;
+ mType = getType();
verifyActiveContainer();
}
@@ -220,7 +227,22 @@
return new LaunchedAppState(mLauncher);
}
} else {
- mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+ final Pattern event;
+ if (mOverview.isLiveTile(mTask)) {
+ event = TASK_START_EVENT_LIVE_TILE;
+ } else if (mType == TaskViewType.DESKTOP) {
+ event = TASK_START_EVENT_DESKTOP;
+ } else {
+ event = TASK_START_EVENT;
+ }
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, event);
+
+ if (mType == TaskViewType.DESKTOP) {
+ try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer(
+ "launched desktop")) {
+ mLauncher.waitForSystemUiObject("desktop_mode_caption");
+ }
+ }
return new LaunchedAppState(mLauncher);
}
}
@@ -273,6 +295,17 @@
return actual.contains(expected);
}
+ private TaskViewType getType() {
+ String resourceName = mTask.getResourceName();
+ if (resourceName.endsWith("task_view_grouped")) {
+ return TaskViewType.GROUPED;
+ } else if (resourceName.endsWith("task_view_desktop")) {
+ return TaskViewType.DESKTOP;
+ } else {
+ return TaskViewType.SINGLE;
+ }
+ }
+
/**
* Enum used to specify which task is retrieved when it is a split task.
*/
@@ -292,4 +325,10 @@
this.iconAppRes = iconAppRes;
}
}
+
+ private enum TaskViewType {
+ SINGLE,
+ GROUPED,
+ DESKTOP
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index 902ad5b..90d32f3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -97,6 +97,29 @@
}
}
+ /**
+ * Taps the Desktop item from the overview task menu and returns the LaunchedAppState
+ * representing the Desktop.
+ */
+ @NonNull
+ public LaunchedAppState tapDesktopMenuItem() {
+ try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
+ "before tapping the desktop menu item")) {
+ mLauncher.executeAndWaitForLauncherStop(
+ () -> mLauncher.clickLauncherObject(
+ mLauncher.findObjectInContainer(mMenu, By.text("Desktop"))),
+ "tapped desktop menu item");
+
+ try (LauncherInstrumentation.Closable ignored2 = mLauncher.addContextLayer(
+ "tapped desktop menu item")) {
+ mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
+ mLauncher.waitForSystemUiObject("desktop_mode_caption");
+ return new LaunchedAppState(mLauncher);
+ }
+ }
+ }
+
/** Returns true if an item matching the given string is present in the menu. */
public boolean hasMenuItem(String expectedMenuItemText) {
UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));
@@ -104,14 +127,6 @@
}
/**
- * Returns the menu item specified by name if present.
- */
- public OverviewTaskMenuItem getMenuItemByName(String menuItemName) {
- return new OverviewTaskMenuItem(mLauncher,
- mLauncher.waitForObjectInContainer(mMenu, By.text(menuItemName)));
- }
-
- /**
* Taps outside task menu to dismiss it.
*/
public void touchOutsideTaskMenuToDismiss() {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
deleted file mode 100644
index e3035bf..0000000
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2023 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 android.graphics.Rect;
-
-import androidx.test.uiautomator.UiObject2;
-
-/** Represents an item in the overview task menu. */
-public class OverviewTaskMenuItem {
-
- private final LauncherInstrumentation mLauncher;
- private final UiObject2 mMenuItem;
-
- OverviewTaskMenuItem(LauncherInstrumentation launcher, UiObject2 menuItem) {
- mLauncher = launcher;
- mMenuItem = menuItem;
- }
-
- /**
- * Returns this menu item's visible bounds.
- */
- public Rect getVisibleBounds() {
- return mMenuItem.getVisibleBounds();
- }
-}