Merge "Remove BubbleBarView.mIsAnimatingNewBubble" 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/statehandlers/DesktopVisibilityController.java b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
index 6c7fe5b..644705b 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java
@@ -25,6 +25,7 @@
import android.util.Log;
import android.view.View;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.Launcher;
@@ -38,6 +39,7 @@
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.Executor;
/**
* Controls the visibility of the workspace and the resumed / paused state when desktop mode
@@ -56,7 +58,7 @@
private boolean mGestureInProgress;
@Nullable
- private IDesktopTaskListener mDesktopTaskListener;
+ private DesktopTaskListenerImpl mDesktopTaskListener;
public DesktopVisibilityController(Launcher launcher) {
mLauncher = launcher;
@@ -66,24 +68,7 @@
* Register a listener with System UI to receive updates about desktop tasks state
*/
public void registerSystemUiListener() {
- mDesktopTaskListener = new IDesktopTaskListener.Stub() {
- @Override
- public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
- MAIN_EXECUTOR.execute(() -> {
- if (displayId == mLauncher.getDisplayId()) {
- if (DEBUG) {
- Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
- }
- setVisibleDesktopTasksCount(visibleTasksCount);
- }
- });
- }
-
- @Override
- public void onStashedChanged(int displayId, boolean stashed) {
- Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
- }
- };
+ mDesktopTaskListener = new DesktopTaskListenerImpl(this, mLauncher.getDisplayId());
SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(mDesktopTaskListener);
}
@@ -92,6 +77,7 @@
*/
public void unregisterSystemUiListener() {
SystemUiProxy.INSTANCE.get(mLauncher).setDesktopTaskListener(null);
+ mDesktopTaskListener.release();
mDesktopTaskListener = null;
}
@@ -355,4 +341,43 @@
*/
void onDesktopVisibilityChanged(boolean visible);
}
+
+ /**
+ * Wrapper for the IDesktopTaskListener stub to prevent lingering references to the launcher
+ * activity via the controller.
+ */
+ private static class DesktopTaskListenerImpl extends IDesktopTaskListener.Stub {
+
+ private DesktopVisibilityController mController;
+ private final int mDisplayId;
+
+ DesktopTaskListenerImpl(@NonNull DesktopVisibilityController controller, int displayId) {
+ mController = controller;
+ mDisplayId = displayId;
+ }
+
+ /**
+ * Clears any references to the controller.
+ */
+ void release() {
+ mController = null;
+ }
+
+ @Override
+ public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) {
+ MAIN_EXECUTOR.execute(() -> {
+ if (mController != null && displayId == mDisplayId) {
+ if (DEBUG) {
+ Log.d(TAG, "desktop visible tasks count changed=" + visibleTasksCount);
+ }
+ mController.setVisibleDesktopTasksCount(visibleTasksCount);
+ }
+ });
+ }
+
+ @Override
+ public void onStashedChanged(int displayId, boolean stashed) {
+ Log.w(TAG, "IDesktopTaskListener: onStashedChanged is deprecated");
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
index c201236..a833ccf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/BaseTaskbarContext.java
@@ -16,19 +16,24 @@
package com.android.launcher3.taskbar;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
+import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
+import com.android.quickstep.SystemUiProxy;
import java.util.ArrayList;
import java.util.List;
// TODO(b/218912746): Share more behavior to avoid all apps context depending directly on taskbar.
/** Base for common behavior between taskbar window contexts. */
-public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext {
+public abstract class BaseTaskbarContext extends ContextThemeWrapper implements ActivityContext,
+ SystemShortcut.BubbleActivityStarter {
protected final LayoutInflater mLayoutInflater;
private final List<OnDeviceProfileChangeListener> mDPChangeListeners = new ArrayList<>();
@@ -48,6 +53,18 @@
return mDPChangeListeners;
}
+ @Override
+ public void showShortcutBubble(ShortcutInfo info) {
+ if (info == null) return;
+ SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
+ }
+
+ @Override
+ public void showAppBubble(Intent intent) {
+ if (intent == null || intent.getPackage() == null) return;
+ SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+ }
+
/** Callback invoked when a drag is initiated within this context. */
public abstract void onDragStart();
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 3981e43..e5edd51 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -225,9 +225,12 @@
// Launcher is resumed during the swipe-to-overview gesture under shell-transitions, so
// avoid updating taskbar state in that situation (when it's non-interactive -- or
// "background") to avoid premature animations.
- if (ENABLE_SHELL_TRANSITIONS && isVisible
- && mLauncher.getStateManager().getState().hasFlag(FLAG_NON_INTERACTIVE)
- && !mLauncher.getStateManager().getState().isTaskbarAlignedWithHotseat(mLauncher)) {
+ LauncherState state = mLauncher.getStateManager().getState();
+ boolean nonInteractiveState = state.hasFlag(FLAG_NON_INTERACTIVE)
+ && !state.isTaskbarAlignedWithHotseat(mLauncher);
+ if ((ENABLE_SHELL_TRANSITIONS
+ && isVisible
+ && (nonInteractiveState || mSkipLauncherVisibilityChange))) {
return null;
}
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/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index 266d0b9..475b516 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -212,7 +212,7 @@
* stashed handle to wrap around the hotseat items.
*/
public Animator createRevealAnimToIsStashed(boolean isStashed, Rect taskbarToHotseatOffsets) {
- Rect visualBounds = new Rect(mControllers.taskbarViewController.getIconLayoutBounds());
+ Rect visualBounds = mControllers.taskbarViewController.getIconLayoutVisualBounds();
float startRadius = mStashedHandleRadius;
if (DisplayController.isTransientTaskbar(mActivity)) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 9d394a8..9c954d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -927,7 +927,7 @@
mControllers.navbarButtonsViewController.updateStateForSysuiFlags(systemUiStateFlags,
fromInit);
boolean isShadeVisible = (systemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
- onNotificationShadeExpandChanged(isShadeVisible, fromInit);
+ onNotificationShadeExpandChanged(isShadeVisible, fromInit || isPhoneMode());
mControllers.taskbarViewController.setRecentsButtonDisabled(
mControllers.navbarButtonsViewController.isRecentsDisabled()
|| isNavBarKidsModeActive());
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
index 6ac862e..8a86402 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
@@ -85,6 +85,9 @@
/** Update values tracked via sysui flags. */
public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) {
+ if (mContext.isPhoneMode()) {
+ return;
+ }
mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0;
if (mContext.isNavBarForceVisible()) {
if (mIsImmersiveMode) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index b697590..e80ad7a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -53,6 +53,7 @@
import com.android.quickstep.util.LogUtils;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
@@ -69,6 +70,9 @@
private static final SystemShortcut.Factory<BaseTaskbarContext>
APP_INFO = SystemShortcut.AppInfo::new;
+ private static final SystemShortcut.Factory<BaseTaskbarContext>
+ BUBBLE = SystemShortcut.BubbleShortcut::new;
+
private final TaskbarActivityContext mContext;
private final PopupDataProvider mPopupDataProvider;
@@ -182,10 +186,13 @@
// Create a Stream of all applicable system shortcuts
private Stream<SystemShortcut.Factory> getSystemShortcuts() {
// append split options to APP_INFO shortcut, the order here will reflect in the popup
- return Stream.concat(
- Stream.of(APP_INFO),
- mControllers.uiController.getSplitMenuOptions()
- );
+ ArrayList<SystemShortcut.Factory> shortcuts = new ArrayList<>();
+ shortcuts.add(APP_INFO);
+ shortcuts.addAll(mControllers.uiController.getSplitMenuOptions().toList());
+ if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+ shortcuts.add(BUBBLE);
+ }
+ return shortcuts.stream();
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 5e7c7ce..4df0223 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -28,6 +28,7 @@
import android.view.animation.PathInterpolator;
import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.SystemUiProxy;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -86,6 +87,10 @@
* Updates the scrim state based on the flags.
*/
public void updateStateForSysuiFlags(@SystemUiStateFlags long stateFlags, boolean skipAnim) {
+ if (mActivity.isPhoneMode()) {
+ // There is no scrim for the bar in the phone mode.
+ return;
+ }
if (isBubbleBarEnabled() && DisplayController.isTransientTaskbar(mActivity)) {
// These scrims aren't used if bubble bar & transient taskbar are active.
return;
@@ -97,10 +102,20 @@
private boolean shouldShowScrim() {
final boolean bubblesExpanded = (mSysUiStateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
boolean isShadeVisible = (mSysUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE) != 0;
+ BubbleControllers bubbleControllers = mActivity.getBubbleControllers();
+ boolean isBubbleControllersPresented = bubbleControllers != null;
+ // when the taskbar is in persistent mode, we hide the task bar icons on bubble bar expand,
+ // which makes the taskbar invisible, so need to check if the bubble bar is not on home
+ // to show the scrim view
+ boolean showScrimForBubbles = bubblesExpanded
+ && !mTaskbarVisible
+ && isBubbleControllersPresented
+ && !DisplayController.isTransientTaskbar(mActivity)
+ && !bubbleControllers.bubbleStashController.isBubblesShowingOnHome();
return bubblesExpanded && !mControllers.navbarButtonsViewController.isImeVisible()
&& !isShadeVisible
&& !mControllers.taskbarStashController.isStashed()
- && mTaskbarVisible;
+ && (mTaskbarVisible || showScrimForBubbles);
}
private float getScrimAlpha() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 5d6fdc1..e33e293 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -1022,6 +1022,10 @@
/** Called when some system ui state has changed. (See SYSUI_STATE_... in QuickstepContract) */
public void updateStateForSysuiFlags(long systemUiStateFlags, boolean skipAnim) {
+ if (mActivity.isPhoneMode()) {
+ return;
+ }
+
long animDuration = TASKBAR_STASH_DURATION;
long startDelay = 0;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 2ada5ba..42bf8db 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -61,6 +61,8 @@
// Initialized in init.
protected TaskbarControllers mControllers;
+ protected boolean mSkipLauncherVisibilityChange;
+
@CallSuper
protected void init(TaskbarControllers taskbarControllers) {
mControllers = taskbarControllers;
@@ -418,4 +420,12 @@
public void setUserIsNotGoingHome(boolean isNotGoingHome) {
mControllers.taskbarStashController.setUserIsNotGoingHome(isNotGoingHome);
}
+
+ /**
+ * Sets whether to prevent taskbar from reacting to launcher visibility during the recents
+ * transition animation.
+ */
+ public void setSkipLauncherVisibilityChange(boolean skip) {
+ mSkipLauncherVisibilityChange = skip;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index e58069a..fc76972 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -669,8 +669,20 @@
return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
}
+ /**
+ * Gets visual bounds of the taskbar view. The visual bounds correspond to the taskbar touch
+ * area, rather than layout placement in the parent view.
+ */
+ public Rect getIconLayoutVisualBounds() {
+ return new Rect(mIconLayoutBounds);
+ }
+
+ /** Gets taskbar layout bounds in parent view. */
public Rect getIconLayoutBounds() {
- return mIconLayoutBounds;
+ Rect actualBounds = new Rect(mIconLayoutBounds);
+ actualBounds.top = getTop();
+ actualBounds.bottom = getBottom();
+ return actualBounds;
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index ffa4819..b8b85d1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -96,7 +96,9 @@
public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4;
public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5;
public static final int ALPHA_INDEX_SMALL_SCREEN = 6;
- private static final int NUM_ALPHA_CHANNELS = 7;
+
+ public static final int ALPHA_INDEX_BUBBLE_BAR = 7;
+ private static final int NUM_ALPHA_CHANNELS = 8;
private static boolean sEnableModelLoadingForTests = true;
@@ -272,6 +274,10 @@
OneShotPreDrawListener.add(mTaskbarView, listener);
}
+ public Rect getIconLayoutVisualBounds() {
+ return mTaskbarView.getIconLayoutVisualBounds();
+ }
+
public Rect getIconLayoutBounds() {
return mTaskbarView.getIconLayoutBounds();
}
@@ -462,14 +468,14 @@
if (mControllers.getSharedState().startTaskbarVariantIsTransient) {
float transY =
mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight
- - mTaskbarView.getIconLayoutBounds().bottom)
+ - mTaskbarView.getIconLayoutVisualBounds().bottom)
- (mPersistentTaskbarDp.taskbarHeight
- mTransientTaskbarDp.taskbarIconSize) / 2f;
taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY);
} else {
float transY =
-mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight
- - mTaskbarView.getIconLayoutBounds().bottom)
+ - mTaskbarView.getIconLayoutVisualBounds().bottom)
- (mTransientTaskbarDp.taskbarHeight
- mTransientTaskbarDp.taskbarIconSize) / 2f;
taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f);
diff --git a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
index 619c9c4..c380c8d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/VoiceInteractionWindowController.kt
@@ -110,7 +110,7 @@
}
fun setIsVoiceInteractionWindowVisible(visible: Boolean, skipAnim: Boolean) {
- if (isVoiceInteractionWindowVisible == visible) {
+ if (isVoiceInteractionWindowVisible == visible || context.isPhoneMode) {
return
}
isVoiceInteractionWindowVisible = visible
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index f16829b..819c473 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -416,6 +416,7 @@
LayoutParams lp = (LayoutParams) getLayoutParams();
lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
setLayoutParams(lp); // triggers a relayout
+ updateBubbleAccessibilityStates();
}
/**
@@ -658,13 +659,25 @@
return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY();
}
- /**
- * Updates the bounds with translation that may have been applied and returns the result.
- */
+ /** Returns the bounds with translation that may have been applied. */
public Rect getBubbleBarBounds() {
- mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize;
- mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY();
- return mBubbleBarBounds;
+ Rect bounds = new Rect(mBubbleBarBounds);
+ bounds.top = getTop() + (int) getTranslationY() + mPointerSize;
+ bounds.bottom = getBottom() + (int) getTranslationY();
+ return bounds;
+ }
+
+ /** Returns the expanded bounds with translation that may have been applied. */
+ public Rect getBubbleBarExpandedBounds() {
+ Rect expandedBounds = getBubbleBarBounds();
+ if (!isExpanded() || isExpanding()) {
+ if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+ expandedBounds.right = expandedBounds.left + (int) expandedWidth();
+ } else {
+ expandedBounds.left = expandedBounds.right - (int) expandedWidth();
+ }
+ }
+ return expandedBounds;
}
/**
@@ -875,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;
@@ -1240,6 +1280,13 @@
}
/**
+ * Returns whether the bubble bar is expanding.
+ */
+ public boolean isExpanding() {
+ return mWidthAnimator.isRunning() && mIsBarExpanded;
+ }
+
+ /**
* Get width of the bubble bar as if it would be expanded.
*
* @return width of the bubble bar in its expanded state, regardless of current width
@@ -1312,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 eb6ff98..3261262 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -72,6 +72,7 @@
private BubbleDragController mBubbleDragController;
private TaskbarStashController mTaskbarStashController;
private TaskbarInsetsController mTaskbarInsetsController;
+ private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider;
private View.OnClickListener mBubbleClickListener;
private View.OnClickListener mBubbleBarClickListener;
private BubbleView.Controller mBubbleViewController;
@@ -110,13 +111,16 @@
R.dimen.bubblebar_icon_size);
}
- public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+ /** Initializes controller. */
+ public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers,
+ TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) {
mBubbleStashController = bubbleControllers.bubbleStashController;
mBubbleBarController = bubbleControllers.bubbleBarController;
mBubbleDragController = bubbleControllers.bubbleDragController;
mTaskbarStashController = controllers.taskbarStashController;
mTaskbarInsetsController = controllers.taskbarInsetsController;
mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
+ mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider;
onBubbleBarConfigurationChanged(/* animate= */ false);
mActivity.addOnDeviceProfileChangeListener(
dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
@@ -354,6 +358,7 @@
if (hidden) {
mBarView.setAlpha(0);
mBarView.setExpanded(false);
+ updatePersistentTaskbar(/* isBubbleBarExpanded = */ false);
}
mActivity.bubbleBarVisibilityChanged(!hidden);
}
@@ -618,6 +623,7 @@
public void setExpanded(boolean isExpanded) {
if (isExpanded != mBarView.isExpanded()) {
mBarView.setExpanded(isExpanded);
+ updatePersistentTaskbar(isExpanded);
if (!isExpanded) {
mSystemUiProxy.collapseBubbles();
} else {
@@ -628,6 +634,25 @@
}
}
+ private void updatePersistentTaskbar(boolean isBubbleBarExpanded) {
+ if (mBubbleStashController.isTransientTaskBar()) return;
+ boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar();
+ mTaskbarViewPropertiesProvider
+ .getIconsAlpha()
+ .animateToValue(hideTaskbar ? 0 : 1)
+ .start();
+ }
+
+ /** Return {@code true} if expanded bubble bar would intersect the taskbar. */
+ public boolean isIntersectingTaskbar() {
+ if (mBarView.isExpanding() || mBarView.isExpanded()) {
+ Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds();
+ return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds);
+ } else {
+ return false;
+ }
+ }
+
/**
* Sets whether the bubble bar should be expanded. This method is used in response to UI events
* from SystemUI.
@@ -761,4 +786,14 @@
pw.println(" Bubble bar view is null!");
}
}
+
+ /** Interface for BubbleBarViewController to get the taskbar view properties. */
+ public interface TaskbarViewPropertiesProvider {
+
+ /** Returns the bounds of the taskbar. */
+ Rect getTaskbarViewBounds();
+
+ /** Returns taskbar icons alpha */
+ MultiPropertyFactory<View>.MultiProperty getIconsAlpha();
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index 8478dc2..e00916a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -15,8 +15,15 @@
*/
package com.android.launcher3.taskbar.bubbles;
+import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_BUBBLE_BAR;
+
+import android.graphics.Rect;
+import android.view.View;
+
import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController.TaskbarViewPropertiesProvider;
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
+import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.RunnableList;
import java.io.PrintWriter;
@@ -79,7 +86,20 @@
bubbleStashedHandleViewController.orElse(null),
taskbarControllers::runAfterInit
);
- bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this);
+ bubbleBarViewController.init(taskbarControllers, /* bubbleControllers = */ this,
+ new TaskbarViewPropertiesProvider() {
+ @Override
+ public Rect getTaskbarViewBounds() {
+ return taskbarControllers.taskbarViewController.getIconLayoutBounds();
+ }
+
+ @Override
+ public MultiPropertyFactory<View>.MultiProperty getIconsAlpha() {
+ return taskbarControllers.taskbarViewController
+ .getTaskbarIconAlpha()
+ .get(ALPHA_INDEX_BUBBLE_BAR);
+ }
+ });
bubbleDragController.init(/* bubbleControllers = */ this);
bubbleDismissController.init(/* bubbleControllers = */ this);
bubbleBarPinController.init(this);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index b2cc369..6085998 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -45,6 +45,7 @@
import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
import static com.android.launcher3.popup.QuickstepSystemShortcut.getSplitSelectShortcutByPosition;
import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
+import static com.android.launcher3.popup.SystemShortcut.BUBBLE_SHORTCUT;
import static com.android.launcher3.popup.SystemShortcut.DONT_SUGGEST_APP;
import static com.android.launcher3.popup.SystemShortcut.INSTALL;
import static com.android.launcher3.popup.SystemShortcut.PRIVATE_PROFILE_INSTALL;
@@ -75,6 +76,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
+import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -215,7 +217,8 @@
import java.util.function.Predicate;
import java.util.stream.Stream;
-public class QuickstepLauncher extends Launcher implements RecentsViewContainer {
+public class QuickstepLauncher extends Launcher implements RecentsViewContainer,
+ SystemShortcut.BubbleActivityStarter {
private static final boolean TRACE_LAYOUTS =
SystemProperties.getBoolean("persist.debug.trace_layouts", false);
private static final String TRACE_RELAYOUT_CLASS =
@@ -466,6 +469,9 @@
if (Flags.enablePrivateSpace()) {
shortcuts.add(UNINSTALL_APP);
}
+ if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+ shortcuts.add(BUBBLE_SHORTCUT);
+ }
return shortcuts.stream();
}
@@ -1403,8 +1409,14 @@
* Launches two apps as an app pair.
*/
public void launchAppPair(AppPairIcon appPairIcon) {
+ // Potentially show the Taskbar education once the app pair launch finishes
mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon,
- CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE);
+ CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE,
+ (success) -> {
+ if (success && mTaskbarUIController != null) {
+ mTaskbarUIController.showEduOnAppLaunch();
+ }
+ });
}
public boolean canStartHomeSafely() {
@@ -1436,6 +1448,18 @@
return true;
}
+ @Override
+ public void showShortcutBubble(ShortcutInfo info) {
+ if (info == null) return;
+ SystemUiProxy.INSTANCE.get(this).showShortcutBubble(info);
+ }
+
+ @Override
+ public void showAppBubble(Intent intent) {
+ if (intent == null || intent.getPackage() == null) return;
+ SystemUiProxy.INSTANCE.get(this).showAppBubble(intent);
+ }
+
private static final class LauncherTaskViewController extends
TaskViewTouchController<QuickstepLauncher> {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 0c2f29b..59e9f05 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -47,7 +47,6 @@
import android.view.IRecentsAnimationRunner;
import android.view.IRemoteAnimationRunner;
import android.view.MotionEvent;
-import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.IOnBackInvokedCallback;
@@ -894,6 +893,36 @@
}
}
+ /**
+ * Tells SysUI to show a shortcut bubble.
+ *
+ * @param info the shortcut info used to create or identify the bubble.
+ */
+ public void showShortcutBubble(ShortcutInfo info) {
+ try {
+ if (mBubbles != null) {
+ mBubbles.showShortcutBubble(info);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call show bubble for shortcut");
+ }
+ }
+
+ /**
+ * Tells SysUI to show a bubble of an app.
+ *
+ * @param intent the intent used to create the bubble.
+ */
+ public void showAppBubble(Intent intent) {
+ try {
+ if (mBubbles != null) {
+ mBubbles.showAppBubble(intent);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call show bubble for app");
+ }
+ }
+
//
// Splitscreen
//
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index f414399..ab80a8c 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -18,6 +18,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static com.android.launcher3.Flags.enableHandleDelayedGestureCallbacks;
+import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
@@ -44,6 +45,7 @@
import com.android.internal.util.ArrayUtils;
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.taskbar.TaskbarUIController;
import com.android.launcher3.util.DisplayController;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.SystemUiFlagUtils;
@@ -334,6 +336,28 @@
options.setTransientLaunch();
}
options.setSourceInfo(ActivityOptions.SourceInfo.TYPE_RECENTS_ANIMATION, eventTime);
+
+ // Notify taskbar that we should skip reacting to launcher visibility change to
+ // avoid a jumping taskbar.
+ TaskbarUIController taskbarUIController = containerInterface.getTaskbarController();
+ if (enableScalingRevealHomeAnimation() && taskbarUIController != null) {
+ taskbarUIController.setSkipLauncherVisibilityChange(true);
+
+ mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+ @Override
+ public void onRecentsAnimationCanceled(
+ @NonNull HashMap<Integer, ThumbnailData> thumbnailDatas) {
+ taskbarUIController.setSkipLauncherVisibilityChange(false);
+ }
+
+ @Override
+ public void onRecentsAnimationFinished(
+ @NonNull RecentsAnimationController controller) {
+ taskbarUIController.setSkipLauncherVisibilityChange(false);
+ }
+ });
+ }
+
mRecentsAnimationStartPending = getSystemUiProxy()
.startRecentsActivity(intent, options, mCallbacks);
if (enableHandleDelayedGestureCallbacks()) {
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index c3d74bb..8478ac9 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -74,6 +74,7 @@
import java.util.Arrays;
import java.util.List;
+import java.util.function.Consumer;
/**
* Controller class that handles app pair interactions: saving, modifying, deleting, etc.
@@ -232,8 +233,11 @@
*
* @param cuj Should be an integer from {@link Cuj} or -1 if no CUJ needs to be logged for jank
* monitoring
+ * @param callback Called after the app pair launch finishes animating, or null if no method is
+ * to be called
*/
- public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
+ public void launchAppPair(AppPairIcon appPairIcon, int cuj,
+ @Nullable Consumer<Boolean> callback) {
WorkspaceItemInfo app1 = appPairIcon.getInfo().getFirstApp();
WorkspaceItemInfo app2 = appPairIcon.getInfo().getSecondApp();
ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
@@ -273,12 +277,19 @@
mSplitSelectStateController.setLaunchingIconView(appPairIcon);
mSplitSelectStateController.launchSplitTasks(
- AppPairsController.convertRankToSnapPosition(app1.rank));
+ AppPairsController.convertRankToSnapPosition(app1.rank), callback);
}
);
}
/**
+ * Launches an app pair but does not specify a callback
+ */
+ public void launchAppPair(AppPairIcon appPairIcon, int cuj) {
+ launchAppPair(appPairIcon, cuj, null);
+ }
+
+ /**
* Returns an AppInfo associated with the app for the given ComponentKey, or null if no such
* package exists in the AllAppsStore.
*/
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 54f2dd3..770ec5a 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -836,7 +836,7 @@
private final int mSplitPlaceholderSize;
private final int mSplitPlaceholderInset;
private ActivityManager.RunningTaskInfo mTaskInfo;
- private ISplitSelectListener mSplitSelectListener;
+ private DesktopSplitSelectListenerImpl mSplitSelectListener;
private Drawable mAppIcon;
public SplitFromDesktopController(QuickstepLauncher launcher,
@@ -847,21 +847,14 @@
R.dimen.split_placeholder_size);
mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
R.dimen.split_placeholder_inset);
- mSplitSelectListener = new ISplitSelectListener.Stub() {
- @Override
- public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
- int splitPosition, Rect taskBounds) {
- MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition,
- taskBounds));
- return true;
- }
- };
+ mSplitSelectListener = new DesktopSplitSelectListenerImpl(this);
SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener);
}
void onDestroy() {
SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener(
mSplitSelectListener);
+ mSplitSelectListener.release();
mSplitSelectListener = null;
}
@@ -954,4 +947,35 @@
}
}
}
+
+ /**
+ * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher
+ * activity via the controller.
+ */
+ private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub {
+
+ private SplitFromDesktopController mController;
+
+ DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) {
+ mController = controller;
+ }
+
+ /**
+ * Clears any references to the controller.
+ */
+ void release() {
+ mController = null;
+ }
+
+ @Override
+ public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
+ int splitPosition, Rect taskBounds) {
+ MAIN_EXECUTOR.execute(() -> {
+ if (mController != null) {
+ mController.enterSplitSelect(taskInfo, splitPosition, taskBounds);
+ }
+ });
+ return true;
+ }
+ }
}
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 0a3351d..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,12 +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
@@ -51,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
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
index f75e542..f7e4576 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt
@@ -24,6 +24,7 @@
import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.android.launcher3.util.NavigationMode
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -31,6 +32,7 @@
import org.junit.runner.RunWith
@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarModeRuleTest {
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
index a709133..a515405 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt
@@ -19,6 +19,7 @@
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.android.launcher3.util.window.WindowManagerProxy
import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy
import com.google.common.truth.Truth.assertThat
@@ -28,6 +29,7 @@
import org.junit.runners.model.Statement
@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarPinningPreferenceRuleTest {
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
index 22d2079..46817d2 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt
@@ -19,6 +19,7 @@
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.Description
@@ -26,6 +27,7 @@
import org.junit.runners.model.Statement
@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarPreferenceRuleTest {
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
index 234e499..5d4fdc5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt
@@ -34,7 +34,7 @@
import org.junit.runners.model.Statement
@RunWith(LauncherMultivalentJUnit::class)
-@EmulatedDevices(["pixelFoldable2023"])
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarUnitTestRuleTest {
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
index ad4b4de..4834d48 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt
@@ -25,6 +25,7 @@
import org.junit.runner.RunWith
@RunWith(LauncherMultivalentJUnit::class)
+@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarWindowSandboxContextTest {
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
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..2122d9a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.android.launcher3.util.rule.TestStabilityRule
+import com.android.launcher3.util.rule.TestStabilityRule.LOCAL
+import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT
+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()
+ }
+
+ @TestStabilityRule.Stability(flavors = LOCAL or PLATFORM_POSTSUBMIT)
+ @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/res/drawable/ic_bubble_button.xml b/res/drawable/ic_bubble_button.xml
new file mode 100644
index 0000000..1ed212e
--- /dev/null
+++ b/res/drawable/ic_bubble_button.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M23,5v8h-2V5H3v14h10v2v0H3c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h18C22.1,3 23,3.9 23,5zM10,8v2.59L5.71,6.29L4.29,7.71L8.59,12H6v2h6V8H10zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/>
+</vector>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index af91b5a..5e1d8a5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -306,6 +306,7 @@
<dimen name="blur_size_medium_outline">2dp</dimen>
<dimen name="blur_size_click_shadow">4dp</dimen>
<dimen name="click_shadow_high_shift">2dp</dimen>
+ <dimen name="app_title_icon_shadow_inset">1dp</dimen>
<!-- Pending widget -->
<dimen name="pending_widget_min_padding">8dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3b458c2..fd724a5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -215,7 +215,8 @@
<string name="dismiss_prediction_label">Don\'t suggest app</string>
<!-- Label for pinning predicted app. -->
<string name="pin_prediction">Pin Prediction</string>
-
+ <!-- Label for bubbling a launcher item. [CHAR_LIMIT=20] -->
+ <string name="bubble">Bubble</string>
<!-- Permissions: -->
<skip />
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 2eb5034..1eccbff 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -873,7 +873,7 @@
if (drawable == null) {
setText(text);
Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
- + ", will just set text instead. text=" + text);
+ + ", will just set text instead.");
return;
}
drawable.setTint(getCurrentTextColor());
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index 960d77a..de1748b 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -20,7 +20,6 @@
import android.content.Context;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -29,7 +28,6 @@
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.compat.AccessibilityManagerCompat;
-import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -189,10 +187,6 @@
* Scrolls this recycler view to the top.
*/
public void scrollToTop() {
- if (TestProtocol.sDebugTracing) {
- Log.d(TestProtocol.PRIVATE_SPACE_SCROLL_FAILURE, "FastScrollRecyclerView#scrollToTop",
- new Exception());
- }
if (mScrollbar != null) {
mScrollbar.reattachThumbToScroll();
}
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index 40e3813..f31bf1e 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -25,6 +25,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.launcher3.Flags;
import com.android.launcher3.LauncherSettings;
@@ -97,6 +98,8 @@
public int options;
+ @Nullable
+ private ShortcutInfo mShortcutInfo = null;
public WorkspaceItemInfo() {
itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
@@ -175,6 +178,9 @@
public void updateFromDeepShortcutInfo(@NonNull final ShortcutInfo shortcutInfo,
@NonNull final Context context) {
+ if (com.android.wm.shell.Flags.enableBubbleAnything()) {
+ mShortcutInfo = shortcutInfo;
+ }
// {@link ShortcutInfo#getActivity} can change during an update. Recreate the intent
intent = ShortcutKey.makeIntent(shortcutInfo);
title = shortcutInfo.getShortLabel();
@@ -204,6 +210,11 @@
: Arrays.stream(persons).map(Person::getKey).sorted().toArray(String[]::new);
}
+ @Nullable
+ public ShortcutInfo getDeepShortcutInfo() {
+ return mShortcutInfo;
+ }
+
/**
* {@code true} if the shortcut is disabled due to its app being a lower version.
*/
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index f7e1168..0c90eb9 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -11,9 +11,11 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.os.Process;
import android.os.UserHandle;
+import android.util.Log;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;
@@ -25,6 +27,7 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.AbstractFloatingViewHelper;
import com.android.launcher3.Flags;
+import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.SecondaryDropTarget;
import com.android.launcher3.Utilities;
@@ -53,6 +56,7 @@
*/
public abstract class SystemShortcut<T extends ActivityContext> extends ItemInfo
implements View.OnClickListener {
+ private static final String TAG = "SystemShortcut";
private final int mIconResId;
protected final int mLabelResId;
@@ -383,4 +387,63 @@
mAbstractFloatingViewHelper.closeOpenViews(mTarget, true,
AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE);
}
+
+ public static final Factory<ActivityContext> BUBBLE_SHORTCUT =
+ (activity, itemInfo, originalView) -> {
+ if ((itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+ && (itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION)
+ && !(itemInfo instanceof WorkspaceItemInfo)) {
+ return null;
+ }
+ return new BubbleShortcut(activity, itemInfo, originalView);
+ };
+
+ public interface BubbleActivityStarter {
+ /** Tell SysUI to show the provided shortcut in a bubble. */
+ void showShortcutBubble(ShortcutInfo info);
+
+ /** Tell SysUI to show the provided intent in a bubble. */
+ void showAppBubble(Intent intent);
+ }
+
+ public static class BubbleShortcut<T extends ActivityContext> extends SystemShortcut<T> {
+
+ private BubbleActivityStarter mStarter;
+
+ public BubbleShortcut(T target, ItemInfo itemInfo, View originalView) {
+ super(R.drawable.ic_bubble_button, R.string.bubble, target,
+ itemInfo, originalView);
+ if (target instanceof BubbleActivityStarter) {
+ mStarter = (BubbleActivityStarter) target;
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ dismissTaskMenuView();
+ if (mStarter == null) {
+ Log.w(TAG, "starter null!");
+ return;
+ }
+ // TODO: handle GroupTask (single) items so that recent items in taskbar work
+ if (mItemInfo instanceof WorkspaceItemInfo) {
+ WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) mItemInfo;
+ ShortcutInfo shortcutInfo = workspaceItemInfo.getDeepShortcutInfo();
+ if (shortcutInfo != null) {
+ mStarter.showShortcutBubble(shortcutInfo);
+ return;
+ }
+ }
+ // If we're here check for an intent
+ Intent intent = mItemInfo.getIntent();
+ if (intent != null) {
+ if (intent.getPackage() == null) {
+ intent.setPackage(mItemInfo.getTargetPackage());
+ }
+ mStarter.showAppBubble(intent);
+ } else {
+ Log.w(TAG, "unable to bubble, no intent: " + mItemInfo);
+ }
+ }
+ }
}
diff --git a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
index 78ce3a2..e317824 100644
--- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
+++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt
@@ -18,9 +18,6 @@
import android.content.Context
import android.util.Log
-import android.view.ViewGroup
-import androidx.annotation.VisibleForTesting
-import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
@@ -34,7 +31,6 @@
import com.android.launcher3.util.Themes
import com.android.launcher3.views.ActivityContext
import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
-import java.lang.IllegalStateException
const val PREINFLATE_ICONS_ROW_COUNT = 4
const val EXTRA_ICONS_COUNT = 2
@@ -44,11 +40,10 @@
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
-class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
+class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
var hasWorkProfile = false
- @VisibleForTesting(otherwise = PROTECTED)
- var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
+ private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
companion object {
private const val TAG = "AllAppsRecyclerViewPool"
@@ -59,7 +54,7 @@
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
- fun preInflateAllAppsViewHolders(context: T) {
+ fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
val preInflateCount = getPreinflateCount(context)
@@ -103,52 +98,31 @@
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
- preInflateAllAppsViewHolders(
- adapter,
- BaseAllAppsAdapter.VIEW_TYPE_ICON,
- activeRv,
- preInflateCount
- ) {
- getPreinflateCount(context)
- }
- }
-
- @VisibleForTesting(otherwise = PROTECTED)
- fun preInflateAllAppsViewHolders(
- adapter: RecyclerView.Adapter<*>,
- viewType: Int,
- parent: ViewGroup,
- preInflationCount: Int,
- preInflationCountProvider: () -> Int
- ) {
- if (preInflationCount <= 0) {
- return
- }
mCancellableTask?.cancel()
var task: CancellableTask<List<ViewHolder>>? = null
task =
CancellableTask(
{
val list: ArrayList<ViewHolder> = ArrayList()
- for (i in 0 until preInflationCount) {
+ for (i in 0 until preInflateCount) {
if (task?.canceled == true) {
break
}
- list.add(adapter.createViewHolder(parent, viewType))
+ list.add(
+ adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
+ )
}
list
},
MAIN_EXECUTOR,
{ viewHolders ->
- // Run preInflationCountProvider again as the needed VH might have changed
- val newPreInflationCount = preInflationCountProvider.invoke()
- for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) {
+ for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
putRecycledView(viewHolders[i])
}
}
)
mCancellableTask = task
- VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
+ VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
}
/**
@@ -169,7 +143,7 @@
* app icons in size of one all apps pages, so that opening all apps don't need to inflate app
* icons.
*/
- fun getPreinflateCount(context: T): Int {
+ fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
index bc66a33..ef66ffe 100644
--- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
+++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java
@@ -19,11 +19,19 @@
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import android.content.Context;
-import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ImageSpan;
import android.util.AttributeSet;
-import android.widget.TextView;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
@@ -45,22 +53,65 @@
public DoubleShadowBubbleTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- mShadowInfo = new ShadowInfo(context, attrs, defStyle);
- setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, mShadowInfo.ambientShadowColor);
+ mShadowInfo = ShadowInfo.Companion.fromContext(context, attrs, defStyle);
+ setShadowLayer(
+ mShadowInfo.getAmbientShadowBlur(),
+ 0,
+ 0,
+ mShadowInfo.getAmbientShadowColor()
+ );
+ }
+
+ @Override
+ public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) {
+ Drawable drawable = getContext().getDrawable(drawableId);
+ if (drawable == null) {
+ setText(text);
+ Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
+ + ", will just set text instead.");
+ return;
+ }
+ drawable.setTint(getCurrentTextColor());
+ int textSize = Math.round(getTextSize());
+ ImageSpan imageSpan;
+ if (!skipDoubleShadow() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ drawable = getDoubleShadowDrawable(drawable, textSize);
+ }
+ drawable.setBounds(0, 0, textSize, textSize);
+ imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
+ // First space will be replaced with Drawable, second space is for space before text.
+ SpannableString spannable = new SpannableString(" " + text);
+ spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ setText(spannable);
+ }
+
+ @RequiresApi(Build.VERSION_CODES.S)
+ private DoubleShadowIconDrawable getDoubleShadowDrawable(
+ @NonNull Drawable drawable, int textSize
+ ) {
+ // add some padding via inset to avoid shadow clipping
+ int iconInsetSize = getContext().getResources()
+ .getDimensionPixelSize(R.dimen.app_title_icon_shadow_inset);
+ return new DoubleShadowIconDrawable(
+ mShadowInfo,
+ drawable,
+ textSize,
+ iconInsetSize
+ );
}
@Override
public void onDraw(Canvas canvas) {
// If text is transparent or shadow alpha is 0, don't draw any shadow
- if (mShadowInfo.skipDoubleShadow(this)) {
+ if (skipDoubleShadow()) {
super.onDraw(canvas);
return;
}
int alpha = Color.alpha(getCurrentTextColor());
// We enhance the shadow by drawing the shadow twice
- getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0,
- getTextShadowColor(mShadowInfo.ambientShadowColor, alpha));
+ getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+ getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha));
drawWithoutDot(canvas);
canvas.save();
@@ -69,10 +120,10 @@
getScrollY() + getHeight());
getPaint().setShadowLayer(
- mShadowInfo.keyShadowBlur,
- mShadowInfo.keyShadowOffsetX,
- mShadowInfo.keyShadowOffsetY,
- getTextShadowColor(mShadowInfo.keyShadowColor, alpha));
+ mShadowInfo.getKeyShadowBlur(),
+ mShadowInfo.getKeyShadowOffsetX(),
+ mShadowInfo.getKeyShadowOffsetY(),
+ getTextShadowColor(mShadowInfo.getKeyShadowColor(), alpha));
drawWithoutDot(canvas);
canvas.restore();
@@ -80,55 +131,30 @@
drawRunningAppIndicatorIfNecessary(canvas);
}
- public static class ShadowInfo {
- public final float ambientShadowBlur;
- public final int ambientShadowColor;
-
- public final float keyShadowBlur;
- public final float keyShadowOffsetX;
- public final float keyShadowOffsetY;
- public final int keyShadowColor;
-
- public ShadowInfo(Context c, AttributeSet attrs, int defStyle) {
-
- TypedArray a = c.obtainStyledAttributes(
- attrs, R.styleable.ShadowInfo, defStyle, 0);
-
- ambientShadowBlur = a.getDimensionPixelSize(
- R.styleable.ShadowInfo_ambientShadowBlur, 0);
- ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0);
-
- keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0);
- keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0);
- keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0);
- keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0);
- a.recycle();
- }
-
- public boolean skipDoubleShadow(TextView textView) {
- int textAlpha = Color.alpha(textView.getCurrentTextColor());
- int keyShadowAlpha = Color.alpha(keyShadowColor);
- int ambientShadowAlpha = Color.alpha(ambientShadowColor);
- if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
- textView.getPaint().clearShadowLayer();
- return true;
- } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
- textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0,
- getTextShadowColor(ambientShadowColor, textAlpha));
- return true;
- } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
- textView.getPaint().setShadowLayer(
- keyShadowBlur,
- keyShadowOffsetX,
- keyShadowOffsetY,
- getTextShadowColor(keyShadowColor, textAlpha));
- return true;
- } else {
- return false;
- }
+ private boolean skipDoubleShadow() {
+ int textAlpha = Color.alpha(getCurrentTextColor());
+ int keyShadowAlpha = Color.alpha(mShadowInfo.getKeyShadowColor());
+ int ambientShadowAlpha = Color.alpha(mShadowInfo.getAmbientShadowColor());
+ if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
+ getPaint().clearShadowLayer();
+ return true;
+ } else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
+ getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
+ getTextShadowColor(mShadowInfo.getAmbientShadowColor(), textAlpha));
+ return true;
+ } else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
+ getPaint().setShadowLayer(
+ mShadowInfo.getKeyShadowBlur(),
+ mShadowInfo.getKeyShadowOffsetX(),
+ mShadowInfo.getKeyShadowOffsetY(),
+ getTextShadowColor(mShadowInfo.getKeyShadowColor(), textAlpha));
+ return true;
+ } else {
+ return false;
}
}
+
// Multiplies the alpha of shadowColor by textAlpha.
private static int getTextShadowColor(int shadowColor, int textAlpha) {
return setColorAlphaBound(shadowColor,
diff --git a/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
new file mode 100644
index 0000000..7ac7c94
--- /dev/null
+++ b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.views
+
+import android.content.res.ColorStateList
+import android.graphics.BlendMode
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.graphics.Shader
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.InsetDrawable
+import android.os.Build.VERSION_CODES
+import androidx.annotation.RequiresApi
+import androidx.annotation.VisibleForTesting
+
+/**
+ * Launcher wrapper for Drawables to provide a double shadow effect. Currently for use with
+ * [DoubleShadowBubbleTextView] to provide a similar shadow to inline icons.
+ */
+@RequiresApi(VERSION_CODES.S)
+class DoubleShadowIconDrawable(
+ private val shadowInfo: ShadowInfo,
+ iconDrawable: Drawable,
+ private val iconSize: Int,
+ iconInsetSize: Int
+) : Drawable() {
+ private val mIconDrawable: InsetDrawable
+ private val mDoubleShadowNode: RenderNode?
+
+ init {
+ mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize)
+ mIconDrawable.setBounds(0, 0, iconSize, iconSize)
+ mDoubleShadowNode = createShadowRenderNode()
+ }
+
+ @VisibleForTesting
+ fun createShadowRenderNode(): RenderNode {
+ val renderNode = RenderNode("DoubleShadowNode")
+ renderNode.setPosition(0, 0, iconSize, iconSize)
+ // Create render effects
+ val ambientShadow =
+ createShadowRenderEffect(
+ shadowInfo.ambientShadowBlur,
+ 0f,
+ 0f,
+ Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+ )
+ val keyShadow =
+ createShadowRenderEffect(
+ shadowInfo.keyShadowBlur,
+ shadowInfo.keyShadowOffsetX,
+ shadowInfo.keyShadowOffsetY,
+ Color.alpha(shadowInfo.keyShadowColor).toFloat()
+ )
+ val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP)
+ renderNode.setRenderEffect(blend)
+ return renderNode
+ }
+
+ @VisibleForTesting
+ fun createShadowRenderEffect(
+ radius: Float,
+ offsetX: Float,
+ offsetY: Float,
+ alpha: Float
+ ): RenderEffect {
+ return RenderEffect.createColorFilterEffect(
+ PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY),
+ RenderEffect.createOffsetEffect(
+ offsetX,
+ offsetY,
+ RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
+ )
+ )
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (canvas.isHardwareAccelerated && mDoubleShadowNode != null) {
+ if (!mDoubleShadowNode.hasDisplayList()) {
+ // Record render node if its display list is not recorded or discarded
+ // (which happens when it's no longer drawn by anything).
+ val recordingCanvas = mDoubleShadowNode.beginRecording()
+ mIconDrawable.draw(recordingCanvas)
+ mDoubleShadowNode.endRecording()
+ }
+ canvas.drawRenderNode(mDoubleShadowNode)
+ }
+ mIconDrawable.draw(canvas)
+ }
+
+ override fun getIntrinsicHeight() = iconSize
+
+ override fun getIntrinsicWidth() = iconSize
+
+ override fun getOpacity() = PixelFormat.TRANSPARENT
+
+ override fun setAlpha(alpha: Int) {
+ mIconDrawable.alpha = alpha
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ mIconDrawable.colorFilter = colorFilter
+ }
+
+ override fun setTint(color: Int) {
+ mIconDrawable.setTint(color)
+ }
+
+ override fun setTintList(tint: ColorStateList?) {
+ mIconDrawable.setTintList(tint)
+ }
+}
diff --git a/src/com/android/launcher3/views/ShadowInfo.kt b/src/com/android/launcher3/views/ShadowInfo.kt
new file mode 100644
index 0000000..4f626ec
--- /dev/null
+++ b/src/com/android/launcher3/views/ShadowInfo.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.views
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.launcher3.R
+
+/**
+ * Launcher data holder for classes such as [DoubleShadowBubbleTextView] to model shadows for
+ * "double shadow" effect.
+ */
+data class ShadowInfo(
+ val ambientShadowBlur: Float,
+ val ambientShadowColor: Int,
+ val keyShadowBlur: Float,
+ val keyShadowOffsetX: Float,
+ val keyShadowOffsetY: Float,
+ val keyShadowColor: Int
+) {
+
+ companion object {
+ /** Constructs instance of ShadowInfo from Context and given attribute set. */
+ @JvmStatic
+ fun fromContext(context: Context, attrs: AttributeSet?, defStyle: Int): ShadowInfo {
+ val styledAttrs =
+ context.obtainStyledAttributes(attrs, R.styleable.ShadowInfo, defStyle, 0)
+ val shadowInfo =
+ ShadowInfo(
+ ambientShadowBlur =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+ .toFloat(),
+ ambientShadowColor =
+ styledAttrs.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0),
+ keyShadowBlur =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+ .toFloat(),
+ keyShadowOffsetX =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+ .toFloat(),
+ keyShadowOffsetY =
+ styledAttrs
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+ .toFloat(),
+ keyShadowColor = styledAttrs.getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+ )
+ styledAttrs.recycle()
+ return shadowInfo
+ }
+ }
+}
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/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index dc3b321..3f4a73a 100644
--- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -173,8 +173,6 @@
public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466";
public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
- public static final String PRIVATE_SPACE_SCROLL_FAILURE = "b/339737008";
-
public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
diff --git a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt b/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
deleted file mode 100644
index 3e6aae2..0000000
--- a/tests/multivalentTests/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPoolTest.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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.recyclerview
-
-import android.content.Context
-import android.view.View
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.launcher3.util.Executors
-import com.android.launcher3.views.ActivityContext
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
-
- private lateinit var underTest: AllAppsRecyclerViewPool<T>
- private lateinit var adapter: RecyclerView.Adapter<*>
-
- @Mock private lateinit var parent: ViewGroup
- @Mock private lateinit var itemView: View
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest = spy(AllAppsRecyclerViewPool())
- adapter =
- object : RecyclerView.Adapter<ViewHolder>() {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
- object : ViewHolder(itemView) {}
-
- override fun getItemCount() = 0
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
- }
- underTest.setMaxRecycledViews(VIEW_TYPE, 20)
- }
-
- @Test
- fun preinflate_success() {
- underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
-
- awaitTasksCompleted()
- assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
- }
-
- @Test
- fun preinflate_not_triggered() {
- underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 }
-
- awaitTasksCompleted()
- assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
- }
-
- @Test
- fun preinflate_cancel_before_runOnMainThread() {
- underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
- assertThat(underTest.mCancellableTask!!.canceled).isFalse()
-
- underTest.clear()
-
- awaitTasksCompleted()
- verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
- assertThat(underTest.mCancellableTask!!.canceled).isTrue()
- assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
- }
-
- @Test
- fun preinflate_cancel_after_run() {
- underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
- assertThat(underTest.mCancellableTask!!.canceled).isFalse()
- awaitTasksCompleted()
-
- underTest.clear()
-
- verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
- assertThat(underTest.mCancellableTask!!.canceled).isTrue()
- assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
- }
-
- private fun awaitTasksCompleted() {
- Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
- Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
- }
-
- companion object {
- private const val VIEW_TYPE: Int = 4
- }
-}
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/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
new file mode 100644
index 0000000..1cee71c
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.ui
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.views.DoubleShadowIconDrawable
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DoubleShadowIconDrawableTest {
+
+ @Test
+ fun `DoubleShadowIconDrawable is setup correctly from given ShadowInfo`() {
+ // Given
+ val shadowInfo: ShadowInfo = mock()
+ val originalDrawable: Drawable = mock()
+ val iconSize = 2
+ val iconInsetSize = 1
+ // When
+ val drawableUnderTest =
+ DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize)
+ // Then
+ assertThat(drawableUnderTest.intrinsicHeight).isEqualTo(iconSize)
+ assertThat(drawableUnderTest.intrinsicWidth).isEqualTo(iconSize)
+ }
+
+ @Test
+ fun `createShadowRenderNode creates RenderNode for shadow effects`() {
+ // Given
+ val shadowInfo =
+ ShadowInfo(
+ ambientShadowBlur = 1f,
+ ambientShadowColor = 2,
+ keyShadowBlur = 3f,
+ keyShadowOffsetX = 4f,
+ keyShadowOffsetY = 5f,
+ keyShadowColor = 6
+ )
+ val originalDrawable: Drawable = mock()
+ val iconSize = 2
+ val iconInsetSize = 1
+ // When
+ val shadowDrawableUnderTest =
+ spy(DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize))
+ shadowDrawableUnderTest.createShadowRenderNode()
+ // Then
+ verify(shadowDrawableUnderTest)
+ .createShadowRenderEffect(
+ shadowInfo.ambientShadowBlur,
+ 0f,
+ 0f,
+ Color.alpha(shadowInfo.ambientShadowColor).toFloat()
+ )
+ verify(shadowDrawableUnderTest)
+ .createShadowRenderEffect(
+ shadowInfo.keyShadowBlur,
+ shadowInfo.keyShadowOffsetX,
+ shadowInfo.keyShadowOffsetY,
+ Color.alpha(shadowInfo.keyShadowColor).toFloat()
+ )
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
new file mode 100644
index 0000000..ef4dc1a
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.ui
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import com.android.launcher3.views.ShadowInfo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadowInfoTest {
+
+ @Test
+ fun `ShadowInfo is created correctly from context`() {
+ // Given
+ val mockContext: Context = mock()
+ val mockAttrs: AttributeSet = mock()
+ val styledAttrs: TypedArray = mock()
+ val expectedShadowInfo =
+ ShadowInfo(
+ ambientShadowBlur = 1f,
+ ambientShadowColor = 2,
+ keyShadowBlur = 3f,
+ keyShadowOffsetX = 4f,
+ keyShadowOffsetY = 5f,
+ keyShadowColor = 6
+ )
+ doReturn(styledAttrs)
+ .whenever(mockContext)
+ .obtainStyledAttributes(mockAttrs, R.styleable.ShadowInfo, 0, 0)
+ doReturn(1)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
+ doReturn(2).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_ambientShadowColor, 0)
+ doReturn(3)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
+ doReturn(4)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
+ doReturn(5)
+ .whenever(styledAttrs)
+ .getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
+ doReturn(6).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
+ // When
+ val actualShadowInfo = ShadowInfo.fromContext(mockContext, mockAttrs, 0)
+ // Then
+ assertThat(actualShadowInfo.ambientShadowBlur).isEqualTo(1)
+ assertThat(actualShadowInfo.ambientShadowColor).isEqualTo(2)
+ assertThat(actualShadowInfo.keyShadowBlur).isEqualTo(3)
+ assertThat(actualShadowInfo.keyShadowOffsetX).isEqualTo(4)
+ assertThat(actualShadowInfo.keyShadowOffsetY).isEqualTo(5)
+ assertThat(actualShadowInfo.keyShadowColor).isEqualTo(6)
+ assertThat(actualShadowInfo).isEqualTo(expectedShadowInfo)
+ }
+}
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();
- }
-}