Merge "Stop mouse click+drag from opening all apps" into main
diff --git a/Android.bp b/Android.bp
index 5b986ab..1e1e0ad 100644
--- a/Android.bp
+++ b/Android.bp
@@ -452,6 +452,7 @@
"AndroidManifest-common.xml",
],
lint: {
+ extra_check_modules: ["Launcher3LintChecker"],
baseline_filename: "lint-baseline.xml",
},
}
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 5186740..72b3c27 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -65,13 +65,6 @@
}
flag {
- name: "enable_taskbar_connected_displays"
- namespace: "launcher"
- description: "Enables connected displays in taskbar."
- bug: "362720616"
-}
-
-flag {
name: "enable_taskbar_customization"
namespace: "launcher"
description: "Enables taskbar customization framework."
diff --git a/checks/Android.bp b/checks/Android.bp
new file mode 100644
index 0000000..dfd701e
--- /dev/null
+++ b/checks/Android.bp
@@ -0,0 +1,46 @@
+// Copyright (C) 2025 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 {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+ name: "Launcher3LintChecker",
+ srcs: ["src/**/*.kt"],
+ plugins: ["auto_service_plugin"],
+ libs: [
+ "auto_service_annotations",
+ "lint_api",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
+
+java_test_host {
+ name: "Launcher3LintCheckerTest",
+ defaults: ["AndroidLintCheckerTestDefaults"],
+ srcs: ["tests/**/*.kt"],
+ data: [
+ ":androidx.annotation_annotation",
+ ":dagger2",
+ ":kotlinx-coroutines-core",
+ ],
+ device_common_data: [
+ ":framework",
+ ],
+ static_libs: [
+ "Launcher3LintChecker",
+ ],
+}
diff --git a/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt
new file mode 100644
index 0000000..37358bb
--- /dev/null
+++ b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UClass
+
+/** Detector to identify custom usage of Android's Dialog within the Launcher3 codebase. */
+class CustomDialogDetector : Detector(), SourceCodeScanner {
+
+ override fun applicableSuperClasses(): List<String> {
+ return listOf(DIALOG_CLASS_NAME)
+ }
+
+ override fun visitClass(context: JavaContext, declaration: UClass) {
+ val superTypeClassNames = declaration.superTypes.mapNotNull { it.resolve()?.qualifiedName }
+ if (superTypeClassNames.contains(DIALOG_CLASS_NAME)) {
+ context.report(
+ ISSUE,
+ declaration,
+ context.getNameLocation(declaration),
+ "Class implements Dialog",
+ )
+ }
+ }
+
+ companion object {
+ private const val DIALOG_CLASS_NAME = "android.app.Dialog"
+
+ @JvmField
+ val ISSUE =
+ Issue.create(
+ id = "IllegalUseOfCustomDialog",
+ briefDescription = "dialogs should not be used in Launcher",
+ explanation =
+ """
+ Don't use custom Dialogs within the launcher code base, instead consider utilizing
+ AbstractFloatingView to display content that should float above the launcher where
+ it can be correctly managed for dismissal.
+ """
+ .trimIndent(),
+ category = Category.CORRECTNESS,
+ priority = 10,
+ severity = Severity.ERROR,
+ implementation =
+ Implementation(CustomDialogDetector::class.java, Scope.JAVA_FILE_SCOPE),
+ )
+ }
+}
diff --git a/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt
new file mode 100644
index 0000000..c77c42b
--- /dev/null
+++ b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 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.internal.launcher3.lint
+
+import CustomDialogDetector
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.android.tools.lint.detector.api.Issue
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class Launcher3IssueRegistry : IssueRegistry() {
+ override val issues: List<Issue>
+ get() = listOf(CustomDialogDetector.ISSUE)
+
+ override val api: Int
+ get() = CURRENT_API
+
+ override val minApi: Int
+ get() = 8
+
+ override val vendor: Vendor =
+ Vendor(
+ vendorName = "Android",
+ feedbackUrl = "http://b/issues/new?component=78010",
+ contact = "abegovic@google.com",
+ )
+}
diff --git a/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt
new file mode 100644
index 0000000..2a37953
--- /dev/null
+++ b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 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.internal.launcher3.lint
+
+import CustomDialogDetector
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+/** Test for [CustomDialogDetector]. */
+class CustomDialogDetectorTest : Launcher3LintDetectorTest() {
+ override fun getDetector(): Detector = CustomDialogDetector()
+
+ override fun getIssues(): List<Issue> = listOf(CustomDialogDetector.ISSUE)
+
+ @Test
+ fun classDoesNotExtendDialog_noViolation() {
+ lint()
+ .files(
+ TestFiles.kotlin(
+ """
+ package test.pkg
+
+ class SomeClass
+ """
+ .trimIndent()
+ ),
+ *androidStubs,
+ )
+ .issues(CustomDialogDetector.ISSUE)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun classDoesExtendDialog_violation() {
+ lint()
+ .files(
+ TestFiles.kotlin(
+ """
+ package test.pkg
+
+ import android.app.Dialog
+
+ class SomeClass(context: Context) : Dialog(context)
+ """
+ .trimIndent()
+ ),
+ *androidStubs,
+ )
+ .issues(CustomDialogDetector.ISSUE)
+ .run()
+ .expect(
+ ("""
+ src/test/pkg/SomeClass.kt:5: Error: Class implements Dialog [IllegalUseOfCustomDialog]
+ class SomeClass(context: Context) : Dialog(context)
+ ~~~~~~~~~
+ 1 errors, 0 warnings
+ """)
+ .trimIndent()
+ )
+ }
+}
diff --git a/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt
new file mode 100644
index 0000000..09085c7
--- /dev/null
+++ b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 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.internal.launcher3.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Abstract class that should be used by any test for launcher 3 lint detectors.
+ *
+ * When you write your test, ensure that you pass [androidStubs] as part of your [TestFiles]
+ * definition.
+ */
+@RunWith(JUnit4::class)
+abstract class Launcher3LintDetectorTest : LintDetectorTest() {
+
+ /**
+ * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+ * in Android Studio has the same result as running the tests in atest
+ */
+ override fun lint(): TestLintTask =
+ super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+
+ companion object {
+ private val libraryNames =
+ arrayOf(
+ "androidx.annotation_annotation.jar",
+ "dagger2.jar",
+ "framework.jar",
+ "kotlinx-coroutines-core.jar",
+ )
+
+ /**
+ * This file contains stubs of framework APIs and System UI classes for testing purposes
+ * only. The stubs are not used in the lint detectors themselves.
+ */
+ val androidStubs =
+ libraryNames
+ .map { TestFiles.LibraryReferenceTestFile(File(it).canonicalFile) }
+ .toTypedArray()
+ }
+}
diff --git a/quickstep/res/drawable/ic_close_option.xml b/quickstep/res/drawable/ic_close_option.xml
new file mode 100644
index 0000000..5681cb5
--- /dev/null
+++ b/quickstep/res/drawable/ic_close_option.xml
@@ -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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z" />
+</vector>
diff --git a/quickstep/res/values-ar/strings.xml b/quickstep/res/values-ar/strings.xml
index a866bb9..281125d 100644
--- a/quickstep/res/values-ar/strings.xml
+++ b/quickstep/res/values-ar/strings.xml
@@ -46,8 +46,7 @@
<string name="hotsaet_tip_prediction_enabled" msgid="2233554377501347650">"تم تفعيل ميزة \"التطبيقات المقترحة\"."</string>
<string name="hotsaet_tip_prediction_disabled" msgid="1506426298884658491">"ميزة \"التطبيقات المقترحة\" غير مفعّلة."</string>
<string name="hotseat_prediction_content_description" msgid="4582028296938078419">"التطبيق المتوقع: <xliff:g id="TITLE">%1$s</xliff:g>"</string>
- <!-- no translation found for gesture_tutorial_title (2750751261768388354) -->
- <skip />
+ <string name="gesture_tutorial_title" msgid="2750751261768388354">"دليل توجيهي للتنقُّل بالإيماءات"</string>
<string name="gesture_tutorial_rotation_prompt_title" msgid="7537946781362766964">"يُرجى تدوير الجهاز"</string>
<string name="gesture_tutorial_rotation_prompt" msgid="1664493449851960691">"يُرجى تدوير جهازك لإكمال الدليل التوجيهي للتنقُّل بالإيماءات."</string>
<string name="back_gesture_feedback_swipe_too_far_from_edge" msgid="4175100312909721217">"تأكَّد من التمرير سريعًا من أقصى الحافة اليسرى أو اليمنى."</string>
diff --git a/quickstep/res/values-et/strings.xml b/quickstep/res/values-et/strings.xml
index bb56954..0af4d9c 100644
--- a/quickstep/res/values-et/strings.xml
+++ b/quickstep/res/values-et/strings.xml
@@ -89,7 +89,7 @@
<string name="allset_title" msgid="5021126669778966707">"Valmis!"</string>
<string name="allset_hint" msgid="459504134589971527">"Avalehele liikumiseks pühkige üles"</string>
<string name="allset_button_hint" msgid="2395219947744706291">"Avakuvale liikumiseks puudutage avakuva nuppu"</string>
- <string name="allset_description_generic" msgid="5385500062202019855">"<xliff:g id="DEVICE">%1$s</xliff:g> on nüüd kasutamiseks valmis"</string>
+ <string name="allset_description_generic" msgid="5385500062202019855">"Teie <xliff:g id="DEVICE">%1$s</xliff:g> on nüüd kasutamiseks valmis."</string>
<string name="default_device_name" msgid="6660656727127422487">"seade"</string>
<string name="allset_navigation_settings" msgid="4713404605961476027"><annotation id="link">"Süsteemi navigeerimisseaded"</annotation></string>
<string name="action_share" msgid="2648470652637092375">"Jaga"</string>
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index d2a7029..8e70a2b 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -30,6 +30,8 @@
<string name="recent_task_option_desktop">Desktop</string>
<!-- Title and content description for an option to move app to external display. -->
<string name="recent_task_option_external_display">Move to external display</string>
+ <!-- Title and content description for an option to close the app [CHAR LIMIT=30] -->
+ <string name="recent_task_option_close">Close</string>
<!-- Title and content description for Desktop tile in Recents screen that contains apps opened inside desktop windowing mode [CHAR LIMIT=NONE] -->
<string name="recent_task_desktop">Desktop</string>
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index dc0f899..1cf7dda 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -23,6 +23,8 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
import static java.util.Collections.emptyList;
import android.appwidget.AppWidgetManager;
@@ -53,6 +55,7 @@
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetCategoryFilter;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import com.android.systemui.animation.back.FlingOnBackAnimationCallback;
@@ -81,6 +84,10 @@
// the intent, then widgets will not be filtered for size.
private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width";
private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height";
+ // Unlike the AppWidgetManager.EXTRA_CATEGORY_FILTER, this filter removes certain categories.
+ // Filter is ignore if it is not a negative value.
+ // Example usage: WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
+ private static final String EXTRA_CATEGORY_EXCLUSION_FILTER = "category_exclusion_filter";
/**
* Widgets currently added by the user in the UI surface.
* <p>This allows widget picker to exclude existing widgets from suggestions.</p>
@@ -120,7 +127,8 @@
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
- private int mWidgetCategoryFilter;
+ private WidgetCategoryFilter mWidgetCategoryInclusionFilter;
+ private WidgetCategoryFilter mWidgetCategoryExclusionFilter;
@Nullable
private String mUiSurface;
// Widgets existing on the host surface.
@@ -194,8 +202,19 @@
getIntent().getIntExtra(EXTRA_DESIRED_WIDGET_HEIGHT, 0);
// Defaults to '0' to indicate that there isn't a category filter.
- mWidgetCategoryFilter =
- getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+ // Negative value indicates it's an exclusion filter (e.g. NOT_KEYGUARD_CATEGORY.inv())
+ // Positive value indicates it's inclusion filter (e.g. HOME_SCREEN or KEYGUARD)
+ // Note: A filter can either be inclusion or exclusion filter; not both.
+ int inclusionFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0);
+ if (inclusionFilter < 0) {
+ Log.w(TAG, "Invalid EXTRA_CATEGORY_FILTER: " + inclusionFilter);
+ }
+ mWidgetCategoryInclusionFilter = new WidgetCategoryFilter(max(0, inclusionFilter));
+ int exclusionFilter = getIntent().getIntExtra(EXTRA_CATEGORY_EXCLUSION_FILTER, 0);
+ if (exclusionFilter > 0) {
+ Log.w(TAG, "Invalid EXTRA_CATEGORY_EXCLUSION_FILTER: " + exclusionFilter);
+ }
+ mWidgetCategoryExclusionFilter = new WidgetCategoryFilter(min(0 , exclusionFilter));
String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE);
if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) {
@@ -436,11 +455,13 @@
widget.user.getIdentifier());
}
- if (mWidgetCategoryFilter > 0 && (info.widgetCategory & mWidgetCategoryFilter) == 0) {
+ if (!mWidgetCategoryInclusionFilter.matches(info.widgetCategory)
+ || !mWidgetCategoryExclusionFilter.matches(info.widgetCategory)) {
return rejectWidget(
widget,
- "doesn't match category filter [filter=%d, widget=%d]",
- mWidgetCategoryFilter,
+ "doesn't match category filter [inclusion=%d, exclusion=%d, widget=%d]",
+ mWidgetCategoryInclusionFilter.getCategoryMask(),
+ mWidgetCategoryExclusionFilter.getCategoryMask(),
info.widgetCategory);
}
@@ -463,7 +484,7 @@
mDesiredWidgetWidth);
}
- final int minWidth = Math.min(info.minResizeWidth, info.minWidth);
+ final int minWidth = min(info.minResizeWidth, info.minWidth);
if (minWidth > mDesiredWidgetWidth) {
return rejectWidget(
widget,
@@ -487,7 +508,7 @@
mDesiredWidgetHeight);
}
- final int minHeight = Math.min(info.minResizeHeight, info.minHeight);
+ final int minHeight = min(info.minResizeHeight, info.minHeight);
if (minHeight > mDesiredWidgetHeight) {
return rejectWidget(
widget,
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 3736e6d..23065b5 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -233,7 +233,8 @@
}
private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
- return Flags.taskbarOverflow() && taskIdsToExclude.contains(task.task1.key.id);
+ return Flags.taskbarOverflow() && task.getTasks().stream().anyMatch(
+ t -> taskIdsToExclude.contains(t.key.id));
}
private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
index 306443e..4581119 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java
@@ -51,6 +51,9 @@
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
+import com.android.quickstep.util.SplitTask;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import java.util.HashMap;
@@ -255,17 +258,24 @@
layoutInflater,
previousTaskView);
- final boolean firstTaskIsLeftTopTask =
- groupTask.mSplitBounds == null
- || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id
- || groupTask.task2 == null;
+ Task task1;
+ Task task2;
+ if (groupTask instanceof SplitTask splitTask) {
+ task1 = splitTask.getTopLeftTask();
+ task2 = splitTask.getBottomRightTask();
+ } else if (groupTask instanceof SingleTask singleTask) {
+ task1 = singleTask.getTask();
+ task2 = null;
+ } else {
+ continue;
+ }
currentTaskView.setThumbnailsForSplitTasks(
- firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2,
- firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1,
+ task1,
+ task2,
updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
updateTasks ? mViewCallbacks::updateIconInBackground : null,
- groupTask.mSplitBounds);
+ groupTask instanceof SplitTask splitTask ? splitTask.getSplitBounds() : null);
previousTaskView = currentTaskView;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 8cb43d2..5af7ff8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -44,6 +44,7 @@
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
import com.android.quickstep.util.SlideInRemoteTransition;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -281,9 +282,10 @@
return -1;
}
RemoteTransition remoteTransition = slideInTransition;
- if (mOnDesktop
- && mControllers.taskbarActivityContext.canUnminimizeDesktopTask(task.task1.key.id)
- ) {
+ boolean canUnminimizeDesktopTask = task instanceof SingleTask singleTask
+ && mControllers.taskbarActivityContext.canUnminimizeDesktopTask(
+ singleTask.getTask().key.id);
+ if (mOnDesktop && canUnminimizeDesktopTask) {
// This app is being unminimized - use our own transition runner.
remoteTransition = new RemoteTransition(
new DesktopAppLaunchTransition(
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 5a8fba6..4143157 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -49,7 +49,7 @@
import com.android.quickstep.HomeVisibilityState;
import com.android.quickstep.RecentsAnimationCallbacks;
import com.android.quickstep.SystemUiProxy;
-import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SplitTask;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -480,8 +480,8 @@
@Override
public void launchSplitTasks(
- @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
- mLauncher.launchSplitTasks(groupTask, remoteTransition);
+ @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) {
+ mLauncher.launchSplitTasks(splitTask, remoteTransition);
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 8a06b11..3963d40 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -156,6 +156,8 @@
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.util.DesktopTask;
import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
+import com.android.quickstep.util.SplitTask;
import com.android.quickstep.views.DesktopTaskView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
@@ -1294,11 +1296,13 @@
mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
- if (tag instanceof GroupTask groupTask) {
+ // TODO: b/316004172, b/343289567: Handle `DesktopTask` and `SplitTask`.
+ if (tag instanceof SingleTask singleTask) {
RemoteTransition remoteTransition =
- (areDesktopTasksVisible() && canUnminimizeDesktopTask(groupTask.task1.key.id))
+ (areDesktopTasksVisible() && canUnminimizeDesktopTask(
+ singleTask.getTask().key.id))
? createDesktopAppLaunchRemoteTransition(AppLaunchType.UNMINIMIZE,
- Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
+ Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON)
: null;
if (areDesktopTasksVisible() && mControllers.uiController.isInOverviewUi()) {
RunnableList runnableList = recents.launchRunningDesktopTaskView();
@@ -1306,12 +1310,12 @@
// launch.
if (runnableList != null) {
runnableList.add(() -> UI_HELPER_EXECUTOR.execute(
- () -> handleGroupTaskLaunch(groupTask, remoteTransition,
+ () -> handleGroupTaskLaunch(singleTask, remoteTransition,
areDesktopTasksVisible(),
DesktopTaskToFrontReason.TASKBAR_TAP)));
}
} else {
- handleGroupTaskLaunch(groupTask, remoteTransition, areDesktopTasksVisible(),
+ handleGroupTaskLaunch(singleTask, remoteTransition, areDesktopTasksVisible(),
DesktopTaskToFrontReason.TASKBAR_TAP);
}
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1481,13 +1485,13 @@
remoteTransition));
return;
}
- if (onDesktop) {
- boolean useRemoteTransition = canUnminimizeDesktopTask(task.task1.key.id);
+ if (onDesktop && task instanceof SingleTask singleTask) {
+ boolean useRemoteTransition = canUnminimizeDesktopTask(singleTask.getTask().key.id);
UI_HELPER_EXECUTOR.execute(() -> {
if (onStartCallback != null) {
onStartCallback.run();
}
- SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id,
+ SystemUiProxy.INSTANCE.get(this).showDesktopApp(singleTask.getTask().key.id,
useRemoteTransition ? remoteTransition : null, toFrontReason);
if (onFinishCallback != null) {
onFinishCallback.run();
@@ -1495,18 +1499,19 @@
});
return;
}
- if (task.task2 == null) {
+ if (task instanceof SingleTask singleTask) {
UI_HELPER_EXECUTOR.execute(() -> {
ActivityOptions activityOptions =
makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
activityOptions.setRemoteTransition(remoteTransition);
ActivityManagerWrapper.getInstance().startActivityFromRecents(
- task.task1.key, activityOptions);
+ singleTask.getTask().key, activityOptions);
});
return;
}
- mControllers.uiController.launchSplitTasks(task, remoteTransition);
+ assert task instanceof SplitTask;
+ mControllers.uiController.launchSplitTasks((SplitTask) task, remoteTransition);
}
/** Returns whether the given task is minimized and can be unminimized. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index a9e8d6d..3a83db2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -80,9 +80,9 @@
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.BubbleTextHolder;
-import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LogUtils;
import com.android.quickstep.util.MultiValueUpdateListener;
+import com.android.quickstep.util.SingleTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
@@ -433,8 +433,8 @@
null, item.user));
}
intent.putExtra(Intent.EXTRA_USER, item.user);
- } else if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
- Task task = groupTask.task1;
+ } else if (tag instanceof SingleTask singleTask) {
+ Task task = singleTask.getTask();
clipDescription = new ClipDescription(task.titleDescription,
new String[] {
ClipDescription.MIMETYPE_APPLICATION_TASK
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 17fb959..36185b1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -115,9 +115,8 @@
private static final Uri NAV_BAR_KIDS_MODE = Settings.Secure.getUriFor(
Settings.Secure.NAV_BAR_KIDS_MODE);
- private final Context mWindowContext;
+ private final Context mParentContext;
private final @Nullable Context mNavigationBarPanelContext;
- private WindowManager mWindowManager;
private final TaskbarNavButtonController mDefaultNavButtonController;
private final ComponentCallbacks mDefaultComponentCallbacks;
@@ -132,6 +131,8 @@
new NonDestroyableScopedUnfoldTransitionProgressProvider();
/** DisplayId - {@link TaskbarActivityContext} map for Connected Display. */
private final SparseArray<TaskbarActivityContext> mTaskbars = new SparseArray<>();
+ /** DisplayId - {@link Context} map for Connected Display. */
+ private final SparseArray<Context> mWindowContexts = new SparseArray<>();
/** DisplayId - {@link FrameLayout} map for Connected Display. */
private final SparseArray<FrameLayout> mRootLayouts = new SparseArray<>();
/** DisplayId - {@link Boolean} map indicating if RootLayout was added to window. */
@@ -243,36 +244,35 @@
Context context,
AllAppsActionManager allAppsActionManager,
TaskbarNavButtonCallbacks navCallbacks) {
- Display display =
- context.getSystemService(DisplayManager.class).getDisplay(context.getDisplayId());
- mWindowContext = context.createWindowContext(display,
- ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
- null);
+ mParentContext = context;
+ createWindowContext(context.getDisplayId());
mAllAppsActionManager = allAppsActionManager;
+ Display display = context.getSystemService(DisplayManager.class).getDisplay(
+ getDefaultDisplayId());
mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
? context.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
: null;
if (enableTaskbarNoRecreate()) {
- mWindowManager = mWindowContext.getSystemService(WindowManager.class);
createTaskbarRootLayout(getDefaultDisplayId());
}
mDefaultNavButtonController = createDefaultNavButtonController(context, navCallbacks);
mDefaultComponentCallbacks = createDefaultComponentCallbacks();
- SettingsCache.INSTANCE.get(mWindowContext)
+ SettingsCache.INSTANCE.get(getPrimaryWindowContext())
.register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
- SettingsCache.INSTANCE.get(mWindowContext)
+ SettingsCache.INSTANCE.get(getPrimaryWindowContext())
.register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
- mWindowContext.registerComponentCallbacks(mDefaultComponentCallbacks);
- mShutdownReceiver.register(mWindowContext, Intent.ACTION_SHUTDOWN);
+ getPrimaryWindowContext().registerComponentCallbacks(mDefaultComponentCallbacks);
+ mShutdownReceiver.register(getPrimaryWindowContext(), Intent.ACTION_SHUTDOWN);
UI_HELPER_EXECUTOR.execute(() -> {
mSharedState.taskbarSystemActionPendingIntent = PendingIntent.getBroadcast(
- mWindowContext,
+ getPrimaryWindowContext(),
SYSTEM_ACTION_ID_TASKBAR,
- new Intent(ACTION_SHOW_TASKBAR).setPackage(mWindowContext.getPackageName()),
+ new Intent(ACTION_SHOW_TASKBAR).setPackage(
+ getPrimaryWindowContext().getPackageName()),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
mTaskbarBroadcastReceiver.register(
- mWindowContext, RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
+ getPrimaryWindowContext(), RECEIVER_NOT_EXPORTED, ACTION_SHOW_TASKBAR);
});
debugWhyTaskbarNotDestroyed("TaskbarManager created");
@@ -285,14 +285,15 @@
return new TaskbarNavButtonController(
context,
navCallbacks,
- SystemUiProxy.INSTANCE.get(mWindowContext),
+ SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()),
new Handler(),
- new ContextualSearchInvoker(mWindowContext));
+ new ContextualSearchInvoker(getPrimaryWindowContext()));
}
private ComponentCallbacks createDefaultComponentCallbacks() {
return new ComponentCallbacks() {
- private Configuration mOldConfig = mWindowContext.getResources().getConfiguration();
+ private Configuration mOldConfig =
+ getPrimaryWindowContext().getResources().getConfiguration();
@Override
public void onConfigurationChanged(Configuration newConfig) {
@@ -302,7 +303,8 @@
"TaskbarManager#mComponentCallbacks.onConfigurationChanged: " + newConfig);
// TODO: adapt this logic to be specific to different displays.
DeviceProfile dp = mUserUnlocked
- ? LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext)
+ ? LauncherAppState.getIDP(getPrimaryWindowContext()).getDeviceProfile(
+ getPrimaryWindowContext())
: null;
int configDiff = mOldConfig.diff(newConfig) & ~SKIP_RECREATE_CONFIG_CHANGES;
@@ -354,6 +356,7 @@
int displayId = mTaskbars.keyAt(i);
destroyTaskbarForDisplay(displayId);
removeTaskbarRootViewFromWindow(displayId);
+ removeWindowContextFromMap(displayId);
}
}
@@ -372,7 +375,8 @@
}
// make this display-specific
DeviceProfile dp = mUserUnlocked ?
- LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
+ LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
+ getWindowContext(displayId)) : null;
if (dp == null || !isTaskbarEnabled(dp)) {
removeTaskbarRootViewFromWindow(displayId);
}
@@ -419,7 +423,8 @@
*/
public void onUserUnlocked() {
mUserUnlocked = true;
- DisplayController.INSTANCE.get(mWindowContext).addChangeListener(mRecreationListener);
+ DisplayController.INSTANCE.get(getPrimaryWindowContext()).addChangeListener(
+ mRecreationListener);
recreateTaskbar();
addTaskbarRootViewToWindow(getDefaultDisplayId());
}
@@ -482,7 +487,8 @@
return ql.getUnfoldTransitionProgressProvider();
}
} else {
- return SystemUiProxy.INSTANCE.get(mWindowContext).getUnfoldTransitionProvider();
+ return SystemUiProxy.INSTANCE.get(
+ getPrimaryWindowContext()).getUnfoldTransitionProvider();
}
return null;
}
@@ -528,7 +534,8 @@
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "recreateTaskbarForDisplay: " + displayId);
// TODO: make this code display specific
DeviceProfile dp = mUserUnlocked ?
- LauncherAppState.getIDP(mWindowContext).getDeviceProfile(mWindowContext) : null;
+ LauncherAppState.getIDP(getWindowContext(displayId)).getDeviceProfile(
+ getWindowContext(displayId)) : null;
// All Apps action is unrelated to navbar unification, so we only need to check DP.
final boolean isLargeScreenTaskbar = dp != null && dp.isTaskbarPresent;
@@ -542,7 +549,7 @@
+ " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
+ " dp.isTaskbarPresent=" + (dp == null ? "null" : dp.isTaskbarPresent));
if (!isTaskbarEnabled || !isLargeScreenTaskbar) {
- SystemUiProxy.INSTANCE.get(mWindowContext)
+ SystemUiProxy.INSTANCE.get(getPrimaryWindowContext())
.notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
if (!isTaskbarEnabled) {
return;
@@ -754,23 +761,24 @@
mRecentsViewContainer = null;
debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
removeActivityCallbacksAndListeners();
- mTaskbarBroadcastReceiver.unregisterReceiverSafely(mWindowContext);
- destroyAllTaskbars();
+ mTaskbarBroadcastReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+
if (mUserUnlocked) {
- DisplayController.INSTANCE.get(mWindowContext).removeChangeListener(
+ DisplayController.INSTANCE.get(getPrimaryWindowContext()).removeChangeListener(
mRecreationListener);
}
- SettingsCache.INSTANCE.get(mWindowContext)
+ SettingsCache.INSTANCE.get(getPrimaryWindowContext())
.unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
- SettingsCache.INSTANCE.get(mWindowContext)
+ SettingsCache.INSTANCE.get(getPrimaryWindowContext())
.unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
- mWindowContext.unregisterComponentCallbacks(mDefaultComponentCallbacks);
- mShutdownReceiver.unregisterReceiverSafely(mWindowContext);
+ getPrimaryWindowContext().unregisterComponentCallbacks(mDefaultComponentCallbacks);
+ mShutdownReceiver.unregisterReceiverSafely(getPrimaryWindowContext());
+ destroyAllTaskbars();
}
public @Nullable TaskbarActivityContext getCurrentActivityContext() {
- return getTaskbarForDisplay(mWindowContext.getDisplayId());
+ return getTaskbarForDisplay(getDefaultDisplayId());
}
public void dumpLogs(String prefix, PrintWriter pw) {
@@ -786,7 +794,6 @@
taskbar.dumpLogs(prefix + "\t\t", pw);
}
}
-
}
private void addTaskbarRootViewToWindow(int displayId) {
@@ -800,8 +807,7 @@
if (!isTaskbarRootLayoutAddedForDisplay(displayId)) {
FrameLayout rootLayout = getTaskbarRootLayoutForDisplay(displayId);
if (rootLayout != null) {
- mWindowManager.addView(getTaskbarRootLayoutForDisplay(displayId),
- taskbar.getWindowLayoutParams());
+ getWindowManager(displayId).addView(rootLayout, taskbar.getWindowLayoutParams());
mAddedRootLayouts.put(displayId, true);
} else {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW,
@@ -822,7 +828,7 @@
}
if (isTaskbarRootLayoutAddedForDisplay(displayId)) {
- mWindowManager.removeViewImmediate(rootLayout);
+ getWindowManager(displayId).removeViewImmediate(rootLayout);
mAddedRootLayouts.put(displayId, false);
removeTaskbarRootLayoutFromMap(displayId);
}
@@ -855,10 +861,10 @@
* Creates a {@link TaskbarActivityContext} for the given display and adds it to the map.
*/
private TaskbarActivityContext createTaskbarActivityContext(DeviceProfile dp, int displayId) {
- TaskbarActivityContext newTaskbar = new TaskbarActivityContext(mWindowContext,
+ TaskbarActivityContext newTaskbar = new TaskbarActivityContext(getWindowContext(displayId),
mNavigationBarPanelContext, dp, mDefaultNavButtonController,
mUnfoldProgressProvider, isDefaultDisplay(displayId),
- SystemUiProxy.INSTANCE.get(mWindowContext));
+ SystemUiProxy.INSTANCE.get(getPrimaryWindowContext()));
addTaskbarToMap(displayId, newTaskbar);
return newTaskbar;
@@ -892,7 +898,7 @@
*/
private void createTaskbarRootLayout(int displayId) {
Log.d(ILLEGAL_ARGUMENT_WM_ADD_VIEW, "createTaskbarRootLayout: " + displayId);
- FrameLayout newTaskbarRootLayout = new FrameLayout(mWindowContext) {
+ FrameLayout newTaskbarRootLayout = new FrameLayout(getWindowContext(displayId)) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// The motion events can be outside the view bounds of task bar, and hence
@@ -958,8 +964,75 @@
Log.d(NULL_TASKBAR_ROOT_LAYOUT_TAG, "mRootLayouts.size()=" + mRootLayouts.size());
}
+ /**
+ * Creates {@link Context} for the taskbar on the specified display and›› adds it to map.
+ * @param displayId The ID of the display for which to create the window context.
+ */
+ private void createWindowContext(int displayId) {
+ DisplayManager displayManager = mParentContext.getSystemService(DisplayManager.class);
+ if (displayManager == null) {
+ return;
+ }
+
+ Display display = displayManager.getDisplay(displayId);
+ if (display != null) {
+ int windowType = (ENABLE_TASKBAR_NAVBAR_UNIFICATION && isDefaultDisplay(displayId))
+ ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL;
+ Context newContext = mParentContext.createWindowContext(display, windowType, null);
+ addWindowContextToMap(displayId, newContext);
+ }
+ }
+
+ /**
+ * Retrieves the window context of the taskbar for the specified display.
+ *
+ * @param displayId The ID of the display for which to retrieve the window context.
+ * @return The Window Context {@link Context} for a given display or {@code null}.
+ */
+ private Context getWindowContext(int displayId) {
+ return mWindowContexts.get(displayId);
+ }
+
+ @VisibleForTesting
+ public Context getPrimaryWindowContext() {
+ return getWindowContext(getDefaultDisplayId());
+ }
+
+ /**
+ * Retrieves the window manager {@link WindowManager} of the taskbar for the specified display.
+ *
+ * @param displayId The ID of the display for which to retrieve the window manager.
+ * @return The window manager {@link WindowManager} for a given display or {@code null}.
+ */
+ private WindowManager getWindowManager(int displayId) {
+ return getWindowContext(displayId).getSystemService(WindowManager.class);
+ }
+
+ /**
+ * Adds the window context {@link Context} to taskbar map, mapped to display ID.
+ *
+ * @param displayId The ID of the display to associate with the taskbar root layout.
+ * @param windowContext The window context {@link Context} to add to the map.
+ */
+ private void addWindowContextToMap(int displayId, @NonNull Context windowContext) {
+ if (!mWindowContexts.contains(displayId)) {
+ mWindowContexts.put(displayId, windowContext);
+ }
+ }
+
+ /**
+ * Removes the window context {@link Context} for given display ID from the taskbar map.
+ *
+ * @param displayId The ID of the display for which to remove the taskbar root layout.
+ */
+ private void removeWindowContextFromMap(int displayId) {
+ if (mWindowContexts.contains(displayId)) {
+ mWindowContexts.delete(displayId);
+ }
+ }
+
private int getDefaultDisplayId() {
- return mWindowContext.getDisplayId();
+ return mParentContext.getDisplayId();
}
/** Temp logs for b/254119092. */
@@ -974,9 +1047,14 @@
boolean activityTaskbarPresent = mActivity != null
&& mActivity.getDeviceProfile().isTaskbarPresent;
- // TODO: make this display specific
- boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(mWindowContext)
- .getDeviceProfile(mWindowContext).isTaskbarPresent;
+ Context windowContext = getWindowContext(displayId);
+ if (windowContext == null) {
+ log.add("window context for displayId" + displayId);
+ return;
+ }
+
+ boolean contextTaskbarPresent = mUserUnlocked && LauncherAppState.getIDP(windowContext)
+ .getDeviceProfile(windowContext).isTaskbarPresent;
if (activityTaskbarPresent == contextTaskbarPresent) {
log.add("mActivity and mWindowContext agree taskbarIsPresent=" + contextTaskbarPresent);
Log.d(TASKBAR_NOT_DESTROYED_TAG, log.toString());
@@ -993,12 +1071,12 @@
log.add("\t\tmActivity.getDeviceProfile().isTaskbarPresent="
+ activityTaskbarPresent);
}
- log.add("\tmWindowContext logs:");
- log.add("\t\tmWindowContext=" + mWindowContext);
- log.add("\t\tmWindowContext.getResources().getConfiguration()="
- + mWindowContext.getResources().getConfiguration());
+ log.add("\tWindowContext logs:");
+ log.add("\t\tWindowContext=" + windowContext);
+ log.add("\t\tWindowContext.getResources().getConfiguration()="
+ + windowContext.getResources().getConfiguration());
if (mUserUnlocked) {
- log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(mWindowContext)"
+ log.add("\t\tLauncherAppState.getIDP().getDeviceProfile(getPrimaryWindowContext())"
+ ".isTaskbarPresent=" + contextTaskbarPresent);
} else {
log.add("\t\tCouldn't get DeviceProfile because !mUserUnlocked");
@@ -1010,8 +1088,4 @@
private final DeviceProfile.OnDeviceProfileChangeListener mDebugActivityDeviceProfileChanged =
dp -> debugWhyTaskbarNotDestroyed("mActivity onDeviceProfileChanged");
- @VisibleForTesting
- public Context getWindowContext() {
- return mWindowContext;
- }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
index 6047999..417ef7e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRecentAppsController.kt
@@ -76,12 +76,14 @@
var shownTasks: List<GroupTask> = emptyList()
private set
+ val shownTaskIds: List<Int>
+ get() = shownTasks.flatMap { shownTask -> shownTask.tasks }.map { it.key.id }
+
/**
- * The task-state of an app, i.e. whether the app has a task and what state
- * that task is in.
+ * The task-state of an app, i.e. whether the app has a task and what state that task is in.
*
- * @property taskId The ID of the task if one exists (i.e. if the state is
- * RUNNING or MINIMIZED), null otherwise (NOT_RUNNING).
+ * @property taskId The ID of the task if one exists (i.e. if the state is RUNNING or
+ * MINIMIZED), null otherwise (NOT_RUNNING).
*/
data class TaskState(val runningAppState: RunningAppState, val taskId: Int? = null)
@@ -214,9 +216,9 @@
return shownHotseatItems.toTypedArray()
}
- private fun getOrderedAndWrappedDesktopTasks(): List<GroupTask> {
+ private fun getOrderedAndWrappedDesktopTasks(): List<SingleTask> {
val tasks = desktopTask?.tasks ?: emptyList()
- // Kind of hacky, we wrap each single task in the Desktop as a GroupTask.
+ // We wrap each task in the Desktop as a `SingleTask`.
val orderFromId = orderedRunningTaskIds.withIndex().associate { (index, id) -> id to index }
val sortedTasks = tasks.sortedWith(compareBy(nullsLast()) { orderFromId[it.key.id] })
return sortedTasks.map { SingleTask(it) }
@@ -286,7 +288,7 @@
}
private fun updateOrderedRunningTaskIds(): MutableList<Int> {
- val desktopTasksAsList = getOrderedAndWrappedDesktopTasks().flatMap { it.tasks }
+ val desktopTasksAsList = getOrderedAndWrappedDesktopTasks().map { it.task }
val desktopTaskIds = desktopTasksAsList.map { it.key.id }
var newOrder =
orderedRunningTaskIds
@@ -311,42 +313,43 @@
val newShownTasks =
if (Flags.enableMultiInstanceMenuTaskbar()) {
val deduplicatedDesktopTasks =
- desktopTasks.distinctBy { Pair(it.task1.key.packageName, it.task1.key.userId) }
+ desktopTasks.distinctBy { Pair(it.task.key.packageName, it.task.key.userId) }
shownTasks
.filter {
- !it.supportsMultipleTasks() &&
- it.task1.key.id in deduplicatedDesktopTasks.map { it.task1.key.id }
+ it is SingleTask &&
+ it.task.key.id in deduplicatedDesktopTasks.map { it.task.key.id }
}
.toMutableList()
.apply {
addAll(
deduplicatedDesktopTasks.filter { currentTask ->
- val currentTaskKey = currentTask.task1.key
- currentTaskKey.id !in shownTasks.map { it.task1.key.id } &&
+ val currentTaskKey = currentTask.task.key
+ currentTaskKey.id !in shownTaskIds &&
shownHotseatItems.none { hotseatItem ->
- hotseatItem.targetPackage == currentTaskKey.packageName &&
- hotseatItem.user.identifier == currentTaskKey.userId
+ currentTask.containsPackage(
+ hotseatItem.targetPackage,
+ hotseatItem.user.identifier,
+ )
}
}
)
}
} else {
- val desktopTaskIds = desktopTasks.map { it.task1.key.id }
+ val desktopTaskIds = desktopTasks.map { it.task.key.id }
val shownHotseatItemTaskIds =
shownHotseatItems.mapNotNull { it as? TaskItemInfo }.map { it.taskId }
shownTasks
- .filter { !it.supportsMultipleTasks() && it.task1.key.id in desktopTaskIds }
+ .filter { it is SingleTask && it.task.key.id in desktopTaskIds }
.toMutableList()
.apply {
addAll(
desktopTasks.filter { desktopTask ->
- desktopTask.task1.key.id !in
- shownTasks.map { shownTask -> shownTask.task1.key.id }
+ desktopTask.task.key.id !in shownTaskIds
}
)
- removeAll { it.task1.key.id in shownHotseatItemTaskIds }
+ removeAll { it is SingleTask && it.task.key.id in shownHotseatItemTaskIds }
}
}
@@ -371,21 +374,28 @@
groupTasks: List<GroupTask>,
shownHotseatItems: List<ItemInfo>,
): List<GroupTask> {
+ // TODO: b/393476333 - Check the behavior of the Taskbar recents section when empty desks
+ // become supported.
return if (Flags.enableMultiInstanceMenuTaskbar()) {
groupTasks.filter { groupTask ->
- val taskKey = groupTask.task1.key
// Keep tasks that are group tasks or unique package name/user combinations
- groupTask.hasMultipleTasks() ||
- shownHotseatItems.none {
- it.targetPackage == taskKey.packageName &&
- it.user.identifier == taskKey.userId
- }
+ when (groupTask) {
+ is SingleTask ->
+ shownHotseatItems.none {
+ groupTask.containsPackage(it.targetPackage, it.user.identifier)
+ }
+
+ else -> true
+ }
}
} else {
val hotseatPackages = shownHotseatItems.map { it.targetPackage }
groupTasks.filter { groupTask ->
- groupTask.hasMultipleTasks() ||
- !hotseatPackages.contains(groupTask.task1.key.packageName)
+ when (groupTask) {
+ is SingleTask -> hotseatPackages.none { groupTask.containsPackage(it) }
+
+ else -> true
+ }
}
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index f29f95d..e5d642d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -39,7 +39,7 @@
import com.android.launcher3.taskbar.bubbles.BubbleBarController;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.SplitConfigurationOptions;
-import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SplitTask;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
@@ -332,7 +332,7 @@
* Launches the given task in split-screen.
*/
public void launchSplitTasks(
- @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) { }
+ @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) { }
/**
* Returns the matching view (if any) in the taskbar.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 457ba3d..a59c9e3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -26,8 +26,6 @@
import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
-import static java.util.function.Predicate.not;
-
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -68,6 +66,7 @@
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
import com.android.quickstep.views.TaskViewType;
import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -398,7 +397,7 @@
.filter(Objects::nonNull)
.toArray(ItemInfo[]::new);
// TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
- recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList();
+ recentTasks = recentTasks.stream().filter(it -> it instanceof SingleTask).toList();
if (taskbarRecentsLayoutTransition()) {
updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks);
@@ -636,9 +635,10 @@
final Set<GroupTask> recentTasksSet = new ArraySet<>(recentTasks);
for (GroupTask task : recentTasks) {
if (mTaskbarOverflowView != null && overflownTasks != null
- && overflownTasks.size() < itemsToAddToOverflow) {
+ && overflownTasks.size() < itemsToAddToOverflow
+ && task instanceof SingleTask singleTask) {
// TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
- overflownTasks.add(task.task1);
+ overflownTasks.add(singleTask.getTask());
if (overflownTasks.size() == itemsToAddToOverflow) {
mTaskbarOverflowView.setItems(overflownTasks);
}
@@ -648,7 +648,7 @@
// Replace any Recent views with the appropriate type if it's not already that type.
final int expectedLayoutResId;
boolean isCollection = false;
- if (task.supportsMultipleTasks()) {
+ if (!(task instanceof SingleTask)) {
if (task.taskViewType == TaskViewType.DESKTOP) {
// TODO(b/316004172): use Desktop tile layout.
expectedLayoutResId = -1;
@@ -712,18 +712,22 @@
&& tagClass.isInstance(getChildAt(mNextViewIndex).getTag());
}
- /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */
+ /** Binds the SingleTask to the BubbleTextView to be ready to present to the user. */
public void applyGroupTaskToBubbleTextView(BubbleTextView btv, GroupTask groupTask) {
- // TODO(b/343289567): support app pairs.
- Task task1 = groupTask.task1;
+ if (!(groupTask instanceof SingleTask singleTask)) {
+ // TODO(b/343289567 and b/316004172): support app pairs and desktop mode.
+ return;
+ }
+
+ Task task = singleTask.getTask();
// TODO(b/344038728): use FastBitmapDrawable instead of Drawable, to get disabled state
// while dragging.
- Drawable taskIcon = groupTask.task1.icon;
+ Drawable taskIcon = task.icon;
if (taskIcon != null) {
taskIcon = taskIcon.getConstantState().newDrawable().mutate();
}
- btv.applyIconAndLabel(taskIcon, task1.titleDescription);
- btv.setTag(groupTask);
+ btv.applyIconAndLabel(taskIcon, task.titleDescription);
+ btv.setTag(singleTask);
}
/**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 0f05887..cbc5d3d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -92,6 +92,7 @@
import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.util.GroupTask;
+import com.android.quickstep.util.SingleTask;
import com.android.systemui.shared.recents.model.Task;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -739,9 +740,9 @@
return mControllers.taskbarRecentAppsController.getRunningAppState(
itemInfo.getTaskId());
}
- if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) {
+ if (tag instanceof SingleTask singleTask) {
return mControllers.taskbarRecentAppsController.getRunningAppState(
- groupTask.task1.key.id);
+ singleTask.getTask().key.id);
}
return BubbleTextView.RunningAppState.NOT_RUNNING;
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index f672840..690dec4 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -65,7 +65,6 @@
import static com.android.quickstep.util.AnimUtils.completeRunnableListCallback;
import static com.android.quickstep.util.SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
-import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -178,10 +177,10 @@
import com.android.quickstep.TouchInteractionService.TISBinder;
import com.android.quickstep.util.ActiveGestureProtoLogProxy;
import com.android.quickstep.util.AsyncClockEventDelegate;
-import com.android.quickstep.util.GroupTask;
import com.android.quickstep.util.LauncherUnfoldAnimationController;
import com.android.quickstep.util.QuickstepOnboardingPrefs;
import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SplitTask;
import com.android.quickstep.util.SplitToWorkspaceController;
import com.android.quickstep.util.SplitWithKeyboardShortcutController;
import com.android.quickstep.util.TISBindHelper;
@@ -1386,33 +1385,20 @@
}
/**
- * Launches the given {@link GroupTask} in splitscreen.
+ * Launches the given {@link SplitTask} in splitscreen.
*/
public void launchSplitTasks(
- @NonNull GroupTask groupTask, @Nullable RemoteTransition remoteTransition) {
- // SplitBounds can be null if coming from Taskbar launch.
- final boolean firstTaskIsLeftTopTask = isFirstTaskLeftTopTask(groupTask);
- // task2 should never be null when calling this method. Allow a crash to catch invalid calls
- Task task1 = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
- Task task2 = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
- mSplitSelectStateController.launchExistingSplitPair(
- null /* launchingTaskView */,
- task1.key.id,
- task2.key.id,
+ @NonNull SplitTask splitTask, @Nullable RemoteTransition remoteTransition) {
+ mSplitSelectStateController.launchExistingSplitPair(null /* launchingTaskView */,
+ splitTask.getTopLeftTask().key.id,
+ splitTask.getBottomRightTask().key.id,
SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
/* callback= */ success -> mSplitSelectStateController.resetState(),
/* freezeTaskList= */ false,
- groupTask.mSplitBounds == null
- ? SNAP_TO_2_50_50
- : groupTask.mSplitBounds.snapPosition,
+ splitTask.getSplitBounds().snapPosition,
remoteTransition);
}
- private static boolean isFirstTaskLeftTopTask(@NonNull GroupTask groupTask) {
- return groupTask.mSplitBounds == null
- || groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
- }
-
/**
* Launches two apps as an app pair.
*/
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index 2b296c8..f582324 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -149,9 +149,9 @@
mOverviewPanel.setFullscreenProgress(progress);
if (progress > UPDATE_SYSUI_FLAGS_THRESHOLD) {
int sysuiFlags = 0;
- TaskView tv = mOverviewPanel.getFirstTaskView();
- if (tv != null) {
- sysuiFlags = tv.getSysUiStatusNavFlags();
+ TaskView firstTaskView = mOverviewPanel.getFirstTaskView();
+ if (firstTaskView != null) {
+ sysuiFlags = firstTaskView.getSysUiStatusNavFlags();
}
mLauncher.getSystemUiController().updateUiState(UI_STATE_FULLSCREEN_TASK, sysuiFlags);
} else {
diff --git a/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt b/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt
new file mode 100644
index 0000000..69feb4a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/widget/picker/WidgetCategoryFilter.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker
+
+/**
+ * A filter that can be applied on the widgetCategory attribute from appwidget-provider to identify
+ * if the widget can be displayed on a specific widget surface.
+ * - Negative value (e.g. "category_a.inv() and category_b.inv()" excludes the widgets with given
+ * categories.
+ * - Positive value (e.g. "category_a or category_b" includes widgets with those categories.
+ * - 0 means no filter.
+ */
+class WidgetCategoryFilter(val categoryMask: Int) {
+ /** Applies the [categoryMask] to return if the [widgetCategory] matches. */
+ fun matches(widgetCategory: Int): Boolean {
+ return if (categoryMask > 0) { // inclusion filter
+ (widgetCategory and categoryMask) != 0
+ } else if (categoryMask < 0) { // exclusion filter
+ (widgetCategory and categoryMask) == widgetCategory
+ } else {
+ true // no filter
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 78e543c..5f8b89a 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -120,6 +120,7 @@
import com.android.launcher3.dragndrop.DragView;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StatefulContainer;
import com.android.launcher3.taskbar.TaskbarThresholdUtils;
@@ -1527,8 +1528,9 @@
.withInputType(mGestureState.isTrackpadGesture()
? SysUiStatsLog.LAUNCHER_UICHANGED__INPUT_TYPE__TRACKPAD
: SysUiStatsLog.LAUNCHER_UICHANGED__INPUT_TYPE__TOUCH);
- if (targetTask != null) {
- logger.withItemInfo(targetTask.getFirstItemInfo());
+ ItemInfo itemInfo;
+ if (targetTask != null && (itemInfo = targetTask.getFirstItemInfo()) != null) {
+ logger.withItemInfo(itemInfo);
}
int pageIndex = endTarget == LAST_TASK || mRecentsView == null
@@ -2369,9 +2371,6 @@
ActiveGestureLog.CompoundString nextTaskLog =
ActiveGestureLog.CompoundString.newEmptyString();
for (TaskContainer container : nextTask.getTaskContainers()) {
- if (container == null) {
- continue;
- }
nextTaskLog.append("[id: %d, pkg: %s] | ",
container.getTask().key.id,
container.getTask().key.getPackageName());
diff --git a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
index c60d3e8..e1e9c99 100644
--- a/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
+++ b/quickstep/src/com/android/quickstep/LauncherSwipeHandlerV2.java
@@ -56,6 +56,7 @@
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.animation.TransitionAnimator;
+import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.InputConsumerController;
import java.util.Collections;
@@ -300,7 +301,9 @@
// Disable if swiping to PIP
return null;
}
- if (sourceTaskView == null || sourceTaskView.getFirstTask().key.getComponent() == null) {
+ Task firstTask;
+ if (sourceTaskView == null || ((firstTask = sourceTaskView.getFirstTask()) == null)
+ || firstTask.key.getComponent() == null) {
// Disable if it's an invalid task
return null;
}
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index ff9c9f6..a594e49 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -120,7 +120,8 @@
TaskShortcutFactory.WELLBEING,
TaskShortcutFactory.SAVE_APP_PAIR,
TaskShortcutFactory.SCREENSHOT,
- TaskShortcutFactory.MODAL
+ TaskShortcutFactory.MODAL,
+ TaskShortcutFactory.CLOSE,
};
/**
@@ -233,7 +234,7 @@
RecentsView overviewPanel = mTaskContainer.getTaskView().getRecentsView();
// Task has already been dismissed
if (overviewPanel == null) return;
- overviewPanel.initiateSplitSelect(mTaskContainer.getTaskView());
+ overviewPanel.initiateSplitSelect(mTaskContainer);
}
protected void saveAppPair() {
@@ -369,7 +370,7 @@
@Override
public void onClick(View view) {
- saveScreenshot(mTaskContainer.getTaskView().getFirstTask());
+ saveScreenshot(mTaskContainer.getTask());
dismissTaskMenuView();
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index ab5e830..2dbd5e9 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -21,6 +21,7 @@
import static android.view.Surface.ROTATION_0;
import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -48,6 +49,7 @@
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.popup.SystemShortcut.AppInfo;
import com.android.launcher3.util.InstantAppResolver;
+import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
@@ -128,20 +130,28 @@
};
class SplitSelectSystemShortcut extends SystemShortcut {
- private final TaskView mTaskView;
+ private final TaskContainer mTaskContainer;
private final SplitPositionOption mSplitPositionOption;
- public SplitSelectSystemShortcut(RecentsViewContainer container, TaskView taskView,
+ public SplitSelectSystemShortcut(RecentsViewContainer container,
+ TaskContainer taskContainer, TaskView taskView,
SplitPositionOption option) {
super(option.iconResId, option.textResId, container, taskView.getFirstItemInfo(),
taskView);
- mTaskView = taskView;
+ mTaskContainer = taskContainer;
mSplitPositionOption = option;
}
@Override
public void onClick(View view) {
- mTaskView.initiateSplitSelect(mSplitPositionOption);
+ RecentsView recentsView = mTaskContainer.getTaskView().getRecentsView();
+ if (recentsView != null) {
+ recentsView.initiateSplitSelect(
+ mTaskContainer,
+ mSplitPositionOption.stagePosition,
+ SplitConfigurationOptions.getLogEventForPosition(
+ mSplitPositionOption.stagePosition));
+ }
}
}
@@ -152,7 +162,6 @@
class SaveAppPairSystemShortcut extends SystemShortcut<RecentsViewContainer> {
private final GroupedTaskView mTaskView;
-
public SaveAppPairSystemShortcut(RecentsViewContainer container, GroupedTaskView taskView,
int iconResId) {
super(iconResId, R.string.save_app_pair, container, taskView.getFirstItemInfo(),
@@ -202,14 +211,14 @@
}
private void startActivity() {
- final Task.TaskKey taskKey = mTaskView.getFirstTask().key;
- final int taskId = taskKey.id;
final ActivityOptions options = makeLaunchOptions(mTarget);
- if (options != null) {
- options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
+ if (options == null) {
+ return;
}
- if (options != null
- && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
+ final Task.TaskKey taskKey = mTaskContainer.getTask().key;
+ final int taskId = taskKey.id;
+ options.setSplashScreenStyle(SplashScreen.SPLASH_SCREEN_STYLE_ICON);
+ if (ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
options)) {
final Runnable animStartedListener = () -> {
// Hide the task view and wait for the window to be resized
@@ -252,8 +261,8 @@
overridePendingAppTransitionMultiThumbFuture(
future, animStartedListener, mHandler, true /* scaleUp */,
taskKey.displayId);
- mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getFirstItemInfo())
- .log(mLauncherEvent);
+ mTarget.getStatsLogManager().logger().withItemInfo(mTaskContainer.getItemInfo())
+ .log(mLauncherEvent);
}
}
@@ -289,6 +298,29 @@
}
}
+ class CloseSystemShortcut extends SystemShortcut {
+ private final TaskContainer mTaskContainer;
+
+ public CloseSystemShortcut(int iconResId, int textResId, RecentsViewContainer container,
+ TaskContainer taskContainer) {
+ super(iconResId, textResId, container, taskContainer.getTaskView().getFirstItemInfo(),
+ taskContainer.getTaskView());
+ mTaskContainer = taskContainer;
+ }
+
+ @Override
+ public void onClick(View view) {
+ TaskView taskView = mTaskContainer.getTaskView();
+ RecentsView<?, ?> recentsView = taskView.getRecentsView();
+ if (recentsView != null) {
+ dismissTaskMenuView();
+ recentsView.dismissTask(taskView, true, true);
+ mTarget.getStatsLogManager().logger().withItemInfo(mTaskContainer.getItemInfo())
+ .log(LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP);
+ }
+ }
+ }
+
/**
* Does NOT add split options in the following scenarios:
* * 1. Taskbar is not present AND aren't at least 2 tasks in overview to show split options for
@@ -327,7 +359,8 @@
return orientationHandler.getSplitPositionOptions(deviceProfile)
.stream()
.map((Function<SplitPositionOption, SystemShortcut>) option ->
- new SplitSelectSystemShortcut(container, taskView, option))
+ new SplitSelectSystemShortcut(container, taskContainer, taskView,
+ option))
.collect(Collectors.toList());
}
};
@@ -420,24 +453,24 @@
private static final String TAG = "PinSystemShortcut";
- private final TaskView mTaskView;
+ private final TaskContainer mTaskContainer;
public PinSystemShortcut(RecentsViewContainer target,
TaskContainer taskContainer) {
super(R.drawable.ic_pin, R.string.recent_task_option_pin, target,
taskContainer.getItemInfo(), taskContainer.getTaskView());
- mTaskView = taskContainer.getTaskView();
+ mTaskContainer = taskContainer;
}
@Override
public void onClick(View view) {
- if (mTaskView.launchAsStaticTile() != null) {
+ if (mTaskContainer.getTaskView().launchAsStaticTile() != null) {
SystemUiProxy.INSTANCE.get(mTarget.asContext()).startScreenPinning(
- mTaskView.getFirstTask().key.id);
+ mTaskContainer.getTask().key.id);
}
dismissTaskMenuView();
- mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getFirstItemInfo())
- .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
+ mTarget.getStatsLogManager().logger().withItemInfo(mTaskContainer.getItemInfo())
+ .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
}
}
@@ -508,4 +541,24 @@
return createSingletonShortcutList(modalStateSystemShortcut);
}
};
+
+ TaskShortcutFactory CLOSE = new TaskShortcutFactory() {
+ @Override
+ public List<SystemShortcut> getShortcuts(RecentsViewContainer container,
+ TaskContainer taskContainer) {
+ return Collections.singletonList(new CloseSystemShortcut(
+ R.drawable.ic_close_option,
+ R.string.recent_task_option_close, container, taskContainer));
+ }
+
+ @Override
+ public boolean showForGroupedTask() {
+ return true;
+ }
+
+ @Override
+ public boolean showForDesktopTask() {
+ return true;
+ }
+ };
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 3133907..e47223b 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -123,8 +123,9 @@
int userId = itemInfo.user.getIdentifier();
if (componentName != null) {
for (TaskView taskView : recentsView.getTaskViews()) {
- if (recentsView.isTaskViewVisible(taskView)) {
- Task.TaskKey key = taskView.getFirstTask().key;
+ Task firstTask = taskView.getFirstTask();
+ if (firstTask != null && recentsView.isTaskViewVisible(taskView)) {
+ Task.TaskKey key = firstTask.key;
if (componentName.equals(key.getComponent()) && userId == key.userId) {
return taskView;
}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index fff7e9b..8d010e2 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -52,6 +52,7 @@
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.RecentsViewContainer;
+import com.android.quickstep.views.TaskContainer;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.Task;
@@ -239,10 +240,10 @@
}
@Override
- public void initiateSplitSelect(TaskView taskView,
+ public void initiateSplitSelect(TaskContainer taskContainer,
@SplitConfigurationOptions.StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
- super.initiateSplitSelect(taskView, stagePosition, splitEvent);
+ super.initiateSplitSelect(taskContainer, stagePosition, splitEvent);
mContainer.getStateManager().goToState(OVERVIEW_SPLIT_SELECT);
}
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index 34b0206..28152ec 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -151,8 +151,8 @@
}
}
- fun setState(state: TaskThumbnailUiState) {
- Log.d(TAG, "viewModelUiState changed from: $uiState to: $state")
+ fun setState(state: TaskThumbnailUiState, taskId: Int? = null) {
+ logDebug("taskId: $taskId - uiState changed from: $uiState to: $state")
if (uiState == state) return
uiState = state
resetViews()
@@ -245,6 +245,10 @@
thumbnailView.imageMatrix = viewModel.getThumbnailPositionState(width, height, isLayoutRtl)
}
+ private fun logDebug(message: String) {
+ Log.d(TAG, "[TaskThumbnailView@${Integer.toHexString(hashCode())}] $message")
+ }
+
private fun maybeCreateHeader() {
if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) {
taskThumbnailViewHeader =
diff --git a/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt b/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt
index 63bd03d..a1ff0ce 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt
+++ b/quickstep/src/com/android/quickstep/util/ActiveTrackpadList.kt
@@ -30,7 +30,7 @@
init {
inputManager.registerInputDeviceListener(this, Executors.UI_HELPER_EXECUTOR.handler)
- inputManager.inputDeviceIds.forEach { deviceId -> onInputDeviceAdded(deviceId) }
+ inputManager.inputDeviceIds.filter(this::isTrackpadDevice).forEach(this::add)
}
override fun onInputDeviceAdded(deviceId: Int) {
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index 6b8650f..f20d7a5 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -128,26 +128,25 @@
& ItemInfoWithIcon.FLAG_NOT_PINNABLE) != 0));
if (!taskView.containsMultipleTasks()
|| hasUnpinnableApp
- || !(taskView instanceof GroupedTaskView)) {
+ || !(taskView instanceof GroupedTaskView groupedTaskView)) {
return false;
}
- GroupedTaskView gtv = (GroupedTaskView) taskView;
- List<TaskContainer> containers = gtv.getTaskContainers();
- ComponentKey taskKey1 = TaskUtils.getLaunchComponentKeyForTask(
- containers.get(0).getTask().key);
- ComponentKey taskKey2 = TaskUtils.getLaunchComponentKeyForTask(
- containers.get(1).getTask().key);
- AppInfo app1 = resolveAppInfoByComponent(taskKey1);
- AppInfo app2 = resolveAppInfoByComponent(taskKey2);
+ ComponentKey leftTopComponentKey = TaskUtils.getLaunchComponentKeyForTask(
+ groupedTaskView.getLeftTopTaskContainer().getTask().key);
+ ComponentKey rightBottomComponentKey = TaskUtils.getLaunchComponentKeyForTask(
+ groupedTaskView.getRightBottomTaskContainer().getTask().key);
+ AppInfo leftTopAppInfo = resolveAppInfoByComponent(leftTopComponentKey);
+ AppInfo rightBottomAppInfo = resolveAppInfoByComponent(rightBottomComponentKey);
- if (app1 == null || app2 == null) {
+ if (leftTopAppInfo == null || rightBottomAppInfo == null) {
// Disallow saving app pairs for apps that don't have a front-door in Launcher
return false;
}
- if (PackageManagerHelper.isSameAppForMultiInstance(app1, app2)) {
- if (!app1.supportsMultiInstance() || !app2.supportsMultiInstance()) {
+ if (PackageManagerHelper.isSameAppForMultiInstance(leftTopAppInfo, rightBottomAppInfo)) {
+ if (!leftTopAppInfo.supportsMultiInstance()
+ || !rightBottomAppInfo.supportsMultiInstance()) {
return false;
}
}
@@ -183,9 +182,8 @@
return;
}
- List<TaskContainer> containers = gtv.getTaskContainers();
List<TaskViewItemInfo> recentsInfos =
- containers.stream().map(TaskContainer::getItemInfo).toList();
+ gtv.getTaskContainers().stream().map(TaskContainer::getItemInfo).toList();
List<WorkspaceItemInfo> apps =
recentsInfos.stream().map(this::resolveAppPairWorkspaceInfo).toList();
diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.kt b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
index 5463cf7..53ea022 100644
--- a/quickstep/src/com/android/quickstep/util/DesktopTask.kt
+++ b/quickstep/src/com/android/quickstep/util/DesktopTask.kt
@@ -20,16 +20,9 @@
/**
* A [Task] container that can contain N number of tasks that are part of the desktop in recent
- * tasks list.
+ * tasks list. Note that desktops can be empty with no tasks in them.
*/
-class DesktopTask(override val tasks: List<Task>) :
- GroupTask(tasks[0], null, null, TaskViewType.DESKTOP) {
-
- override fun containsTask(taskId: Int) = tasks.any { it.key.id == taskId }
-
- override fun hasMultipleTasks() = tasks.size > 1
-
- override fun supportsMultipleTasks() = true
+class DesktopTask(tasks: List<Task>) : GroupTask(tasks, TaskViewType.DESKTOP) {
override fun copy() = DesktopTask(tasks)
diff --git a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
index e1a8578..455b312 100644
--- a/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
+++ b/quickstep/src/com/android/quickstep/util/ExternalDisplays.kt
@@ -17,6 +17,7 @@
package com.android.quickstep.util
import android.view.Display.DEFAULT_DISPLAY
+import android.view.Display.INVALID_DISPLAY
import com.android.systemui.shared.recents.model.Task
/** Whether this displayId belongs to an external display */
@@ -25,7 +26,14 @@
/** Returns displayId of this [Task], default to [DEFAULT_DISPLAY] */
val Task?.displayId
- get() = this?.key?.displayId ?: DEFAULT_DISPLAY
+ get() =
+ this?.key?.displayId.let { displayId ->
+ when (displayId) {
+ null -> DEFAULT_DISPLAY
+ INVALID_DISPLAY -> DEFAULT_DISPLAY
+ else -> displayId
+ }
+ }
/** Returns if this task belongs tto [DEFAULT_DISPLAY] */
val Task?.isExternalDisplay
diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.kt b/quickstep/src/com/android/quickstep/util/GroupTask.kt
index d5bbcd3..49c37dc 100644
--- a/quickstep/src/com/android/quickstep/util/GroupTask.kt
+++ b/quickstep/src/com/android/quickstep/util/GroupTask.kt
@@ -15,7 +15,6 @@
*/
package com.android.quickstep.util
-import androidx.annotation.VisibleForTesting
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
@@ -25,47 +24,26 @@
* An abstract class for creating [Task] containers that can be [SingleTask]s, [SplitTask]s, or
* [DesktopTask]s in the recent tasks list.
*/
-abstract class GroupTask
-@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
-constructor(
- @Deprecated("Prefer using `getTasks()` instead") @JvmField val task1: Task,
- @Deprecated("Prefer using `getTasks()` instead") @JvmField val task2: Task?,
- @JvmField val mSplitBounds: SplitConfigurationOptions.SplitBounds?,
- @JvmField val taskViewType: TaskViewType,
-) {
- protected constructor(
- task1: Task,
- task2: Task?,
- splitBounds: SplitConfigurationOptions.SplitBounds?,
- ) : this(
- task1,
- task2,
- splitBounds,
- if (task2 != null) TaskViewType.GROUPED else TaskViewType.SINGLE,
- )
-
- open fun containsTask(taskId: Int) =
- task1.key.id == taskId || (task2 != null && task2.key.id == taskId)
+abstract class GroupTask(val tasks: List<Task>, @JvmField val taskViewType: TaskViewType) {
+ fun containsTask(taskId: Int) = tasks.any { it.key.id == taskId }
/**
* Returns true if a task in this group has a package name that matches the given `packageName`.
*/
- fun containsPackage(packageName: String) = tasks.any { it.key.packageName == packageName }
+ fun containsPackage(packageName: String?) = tasks.any { it.key.packageName == packageName }
- open fun hasMultipleTasks() = task2 != null
+ /**
+ * Returns true if a task in this group has a package name that matches the given `packageName`,
+ * and its user ID matches the given `userId`.
+ */
+ fun containsPackage(packageName: String?, userId: Int) =
+ tasks.any { it.key.packageName == packageName && it.key.userId == userId }
- /** Returns whether this task supports multiple tasks or not. */
- open fun supportsMultipleTasks() = taskViewType == TaskViewType.GROUPED
-
- /** Returns a List of all the Tasks in this GroupTask */
- open val tasks: List<Task>
- get() = listOfNotNull(task1, task2)
+ fun isEmpty() = tasks.isEmpty()
/** Creates a copy of this instance */
abstract fun copy(): GroupTask
- override fun toString() = "type=$taskViewType task1=$task1 task2=$task2"
-
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o !is GroupTask) return false
@@ -76,11 +54,14 @@
}
/** A [Task] container that must contain exactly one task in the recent tasks list. */
-class SingleTask(task: Task) :
- GroupTask(task, task2 = null, mSplitBounds = null, TaskViewType.SINGLE) {
- override fun copy() = SingleTask(task1)
+class SingleTask(task: Task) : GroupTask(listOf(task), TaskViewType.SINGLE) {
- override fun toString() = "type=$taskViewType task=$task1"
+ val task: Task
+ get() = tasks[0]
+
+ override fun copy() = SingleTask(task)
+
+ override fun toString() = "type=$taskViewType task=$task"
override fun equals(o: Any?): Boolean {
if (this === o) return true
@@ -93,19 +74,26 @@
* A [Task] container that must contain exactly two tasks and split bounds to represent an app-pair
* in the recent tasks list.
*/
-class SplitTask(task1: Task, task2: Task, splitBounds: SplitConfigurationOptions.SplitBounds) :
- GroupTask(task1, task2, splitBounds, TaskViewType.GROUPED) {
+class SplitTask(task1: Task, task2: Task, val splitBounds: SplitConfigurationOptions.SplitBounds) :
+ GroupTask(listOf(task1, task2), TaskViewType.GROUPED) {
- override fun copy() = SplitTask(task1, task2!!, mSplitBounds!!)
+ val topLeftTask: Task
+ get() = if (splitBounds.leftTopTaskId == tasks[0].key.id) tasks[0] else tasks[1]
- override fun toString() = "type=$taskViewType task1=$task1 task2=$task2"
+ val bottomRightTask: Task
+ get() = if (topLeftTask == tasks[0]) tasks[1] else tasks[0]
+
+ override fun copy() = SplitTask(tasks[0], tasks[1], splitBounds)
+
+ override fun toString() =
+ "type=$taskViewType topLeftTask=$topLeftTask bottomRightTask=$bottomRightTask"
override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o !is SplitTask) return false
- if (mSplitBounds!! != o.mSplitBounds!!) return false
+ if (splitBounds != o.splitBounds) return false
return super.equals(o)
}
- override fun hashCode() = Objects.hash(super.hashCode(), mSplitBounds)
+ override fun hashCode() = Objects.hash(super.hashCode(), splitBounds)
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 9b4c772..0182969 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -749,8 +749,8 @@
// launcher side animation)
val leftTopApp =
leafRoots.single { change ->
- (isLeftRightSplit && change.endAbsBounds.left == 0) ||
- (!isLeftRightSplit && change.endAbsBounds.top == 0)
+ (isLeftRightSplit && change.endAbsBounds.left <= 0) ||
+ (!isLeftRightSplit && change.endAbsBounds.top <= 0)
}
val dividerPos =
if (isLeftRightSplit) leftTopApp.endAbsBounds.right
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 5f8b4d9..fd8b356 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -71,7 +71,6 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.apppairs.AppPairIcon;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
@@ -260,7 +259,7 @@
GroupTask groupTask = taskGroups.get(i);
if (isInstanceOfAppPair(
groupTask, componentKeys.get(0), componentKeys.get(1))) {
- lastActiveTasks[0] = groupTask.task1;
+ lastActiveTasks[0] = ((SplitTask) groupTask).getTopLeftTask();
break;
}
}
@@ -314,11 +313,15 @@
*/
public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1,
@NonNull ComponentKey componentKey2) {
- return ((isInstanceOfComponent(groupTask.task1, componentKey1)
- && isInstanceOfComponent(groupTask.task2, componentKey2))
- ||
- (isInstanceOfComponent(groupTask.task1, componentKey2)
- && isInstanceOfComponent(groupTask.task2, componentKey1)));
+ if (groupTask instanceof SplitTask splitTask) {
+ return ((isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey1)
+ && isInstanceOfComponent(splitTask.getBottomRightTask(), componentKey2))
+ ||
+ (isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey2)
+ && isInstanceOfComponent(splitTask.getBottomRightTask(),
+ componentKey1)));
+ }
+ return false;
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index 38ffe50..229c8f5 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -55,6 +55,12 @@
private val MINIMUM_RATIO_TO_SHOW_ICON = 0.2f
+ val leftTopTaskContainer: TaskContainer
+ get() = taskContainers[0]
+
+ val rightBottomTaskContainer: TaskContainer
+ get() = taskContainers[1]
+
// TODO(b/336612373): Support new TTV for GroupedTaskView
var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null
private set
@@ -72,8 +78,8 @@
val splitBoundsConfig = splitBoundsConfig ?: return
val inSplitSelection = getThisTaskCurrentlyInSplitSelection() != INVALID_TASK_ID
pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds(
- taskContainers[0].snapshotView,
- taskContainers[1].snapshotView,
+ leftTopTaskContainer.snapshotView,
+ rightBottomTaskContainer.snapshotView,
widthSize,
heightSize,
splitBoundsConfig,
@@ -165,10 +171,10 @@
val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2
// setMaxWidth() needs to be called before mIconView.setIconOrientation which is
// called in the super below.
- (taskContainers[0].iconView as IconAppChipView).setMaxWidth(
+ (leftTopTaskContainer.iconView as IconAppChipView).setMaxWidth(
groupedTaskViewSizes.first.x - iconMargins
)
- (taskContainers[1].iconView as IconAppChipView).setMaxWidth(
+ (rightBottomTaskContainer.iconView as IconAppChipView).setMaxWidth(
groupedTaskViewSizes.second.x - iconMargins
)
}
@@ -189,16 +195,12 @@
if (deviceProfile.isLeftRightSplit) splitBoundsConfig.leftTaskPercent
else splitBoundsConfig.topTaskPercent
val bottomRightTaskPercent = 1 - topLeftTaskPercent
- taskContainers[0]
- .iconView
- .setFlexSplitAlpha(
- if (topLeftTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON) 0f else 1f
- )
- taskContainers[1]
- .iconView
- .setFlexSplitAlpha(
- if (bottomRightTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON) 0f else 1f
- )
+ leftTopTaskContainer.iconView.setFlexSplitAlpha(
+ if (topLeftTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON) 0f else 1f
+ )
+ rightBottomTaskContainer.iconView.setFlexSplitAlpha(
+ if (bottomRightTaskPercent < MINIMUM_RATIO_TO_SHOW_ICON) 0f else 1f
+ )
}
if (enableOverviewIconMenu()) {
@@ -210,8 +212,8 @@
layoutParams.height,
)
pagedOrientationHandler.setSplitIconParams(
- taskContainers[0].iconView.asView(),
- taskContainers[1].iconView.asView(),
+ leftTopTaskContainer.iconView.asView(),
+ rightBottomTaskContainer.iconView.asView(),
taskIconHeight,
groupedTaskViewSizes.first.x,
groupedTaskViewSizes.first.y,
@@ -224,11 +226,11 @@
)
} else {
pagedOrientationHandler.setSplitIconParams(
- taskContainers[0].iconView.asView(),
- taskContainers[1].iconView.asView(),
+ leftTopTaskContainer.iconView.asView(),
+ rightBottomTaskContainer.iconView.asView(),
taskIconHeight,
- taskContainers[0].snapshotView.measuredWidth,
- taskContainers[0].snapshotView.measuredHeight,
+ leftTopTaskContainer.snapshotView.measuredWidth,
+ leftTopTaskContainer.snapshotView.measuredHeight,
measuredHeight,
measuredWidth,
isRtl,
@@ -288,8 +290,8 @@
recentsView?.let {
it.splitSelectController.launchExistingSplitPair(
if (launchingExistingTaskView) this else null,
- taskContainers[0].task.key.id,
- taskContainers[1].task.key.id,
+ leftTopTaskContainer.task.key.id,
+ rightBottomTaskContainer.task.key.id,
STAGE_POSITION_TOP_OR_LEFT,
callback,
isQuickSwitch,
@@ -319,14 +321,14 @@
// checks below aren't reliable since both of those views may be gone/transformed
val initSplitTaskId = getThisTaskCurrentlyInSplitSelection()
if (initSplitTaskId != INVALID_TASK_ID) {
- return if (initSplitTaskId == taskContainers[0].task.key.id) 1 else 0
+ return if (initSplitTaskId == leftTopTaskContainer.task.key.id) 1 else 0
}
}
// Check which of the two apps was selected
if (
- taskContainers[1].iconView.asView().containsPoint(lastTouchDownPosition) ||
- taskContainers[1].snapshotView.containsPoint(lastTouchDownPosition)
+ rightBottomTaskContainer.iconView.asView().containsPoint(lastTouchDownPosition) ||
+ rightBottomTaskContainer.snapshotView.containsPoint(lastTouchDownPosition)
) {
return 1
}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 9be462c..c6bd677 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -36,7 +36,6 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.desktop.DesktopRecentsTransitionController;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.statehandlers.DepthController;
@@ -127,9 +126,11 @@
// If Launcher needs to return to split select state, do it now, after the icon has updated.
if (mContainer.hasPendingSplitSelectInfo()) {
PendingSplitSelectInfo recoveryData = mContainer.getPendingSplitSelectInfo();
- if (recoveryData.getStagedTaskId() == taskId) {
+ TaskContainer taskContainer;
+ if (recoveryData != null && recoveryData.getStagedTaskId() == taskId && (taskContainer =
+ mUtils.getTaskContainerById(taskId)) != null) {
initiateSplitSelect(
- getTaskViewByTaskId(recoveryData.getStagedTaskId()),
+ taskContainer,
recoveryData.getStagePosition(), recoveryData.getSource()
);
mContainer.finishSplitSelectRecovery();
@@ -240,10 +241,10 @@
}
@Override
- public void initiateSplitSelect(TaskView taskView,
+ public void initiateSplitSelect(TaskContainer taskContainer,
@SplitConfigurationOptions.StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
- super.initiateSplitSelect(taskView, stagePosition, splitEvent);
+ super.initiateSplitSelect(taskContainer, stagePosition, splitEvent);
getStateManager().goToState(LauncherState.OVERVIEW_SPLIT_SELECT);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 1c8299d..9c8b249 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -213,9 +213,11 @@
import com.android.quickstep.util.LayoutUtils;
import com.android.quickstep.util.RecentsAtomicAnimationFactory;
import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SingleTask;
import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps;
import com.android.quickstep.util.SplitAnimationTimings;
import com.android.quickstep.util.SplitSelectStateController;
+import com.android.quickstep.util.SplitTask;
import com.android.quickstep.util.SurfaceTransaction;
import com.android.quickstep.util.SurfaceTransactionApplier;
import com.android.quickstep.util.TaskGridNavHelper;
@@ -652,13 +654,13 @@
return;
}
- TaskView taskView = getTaskViewByTaskId(taskId);
- if (taskView == null) {
- Log.d(TAG, "onTaskRemoved: " + taskId + ", no associated TaskView");
+ TaskContainer taskContainer = mUtils.getTaskContainerById(taskId);
+ if (taskContainer == null) {
+ Log.d(TAG, "onTaskRemoved: " + taskId + ", no associated Task");
return;
}
Log.d(TAG, "onTaskRemoved: " + taskId);
- Task.TaskKey taskKey = taskView.getFirstTask().key;
+ Task.TaskKey taskKey = taskContainer.getTask().key;
UI_HELPER_EXECUTOR.execute(new CancellableTask<>(
() -> PackageManagerWrapper.getInstance()
.getActivityInfo(taskKey.getComponent(), taskKey.userId) == null,
@@ -847,7 +849,7 @@
private final RecentsViewModel mRecentsViewModel;
private final RecentsViewModelHelper mHelper;
- private final RecentsViewUtils mUtils = new RecentsViewUtils(this);
+ protected final RecentsViewUtils mUtils = new RecentsViewUtils(this);
private final Matrix mTmpMatrix = new Matrix();
@@ -1117,7 +1119,7 @@
TaskView taskView = getTaskViewByTaskId(taskId);
if (taskView != null) {
for (TaskContainer container : taskView.getTaskContainers()) {
- if (container == null || taskId != container.getTask().key.id) {
+ if (taskId != container.getTask().key.id) {
continue;
}
container.getThumbnailViewDeprecated().setThumbnail(container.getTask(),
@@ -1132,9 +1134,10 @@
@Override
public void onTaskIconChanged(@NonNull String pkg, @NonNull UserHandle user) {
for (TaskView taskView : getTaskViews()) {
- Task task = taskView.getFirstTask();
- if (pkg.equals(task.key.getPackageName()) && task.key.userId == user.getIdentifier()) {
- task.icon = null;
+ Task firstTask = taskView.getFirstTask();
+ if (firstTask != null && pkg.equals(firstTask.key.getPackageName())
+ && firstTask.key.userId == user.getIdentifier()) {
+ firstTask.icon = null;
if (taskView.getTaskContainers().stream().anyMatch(
container -> container.getIconView().getDrawable() != null)) {
taskView.onTaskListVisibilityChanged(true /* visible */);
@@ -1951,7 +1954,7 @@
GroupTask groupTask = taskGroups.get(i);
boolean containsStagedTask = stagedTaskIdToBeRemoved != INVALID_TASK_ID
&& groupTask.containsTask(stagedTaskIdToBeRemoved);
- boolean shouldSkipGroupTask = containsStagedTask && !groupTask.hasMultipleTasks();
+ boolean shouldSkipGroupTask = containsStagedTask && groupTask instanceof SingleTask;
if ((isSplitSelectionActive() && groupTask.taskViewType == TaskViewType.DESKTOP)
|| shouldSkipGroupTask) {
@@ -1965,25 +1968,27 @@
// to be a temporary container for the remaining task.
TaskView taskView = getTaskViewFromPool(
containsStagedTask ? TaskViewType.SINGLE : groupTask.taskViewType);
- if (taskView instanceof GroupedTaskView) {
- boolean firstTaskIsLeftTopTask =
- groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
- Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
- Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1;
- ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
- mTaskOverlayFactory, groupTask.mSplitBounds);
- } else if (taskView instanceof DesktopTaskView) {
+ if (taskView instanceof GroupedTaskView groupedTaskView) {
+ var splitTask = (SplitTask) groupTask;
+ groupedTaskView.bind(splitTask.getTopLeftTask(),
+ splitTask.getBottomRightTask(), mOrientationState,
+ mTaskOverlayFactory, splitTask.getSplitBounds());
+ } else if (taskView instanceof DesktopTaskView desktopTaskView) {
// Minimized tasks should not be shown in Overview
List<Task> nonMinimizedTasks =
groupTask.getTasks().stream()
.filter(task -> !task.isMinimized)
.toList();
- ((DesktopTaskView) taskView).bind(nonMinimizedTasks, mOrientationState,
+ desktopTaskView.bind(nonMinimizedTasks, mOrientationState,
mTaskOverlayFactory);
- } else {
- Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2
- : groupTask.task1;
+ } else if (groupTask instanceof SplitTask splitTask) {
+ Task task = splitTask.getTopLeftTask().key.id == stagedTaskIdToBeRemoved
+ ? splitTask.getBottomRightTask()
+ : splitTask.getTopLeftTask();
taskView.bind(task, mOrientationState, mTaskOverlayFactory);
+ } else {
+ taskView.bind(((SingleTask) groupTask).getTask(), mOrientationState,
+ mTaskOverlayFactory);
}
addView(taskView);
@@ -4084,6 +4089,7 @@
} else {
removeTaskInternal(dismissedTaskView);
}
+ // TODO(b/391918297): Logging when the TaskView does not have tasks as well.
mContainer.getStatsLogManager().logger()
.withItemInfo(dismissedTaskView.getFirstItemInfo())
.log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
@@ -5171,18 +5177,20 @@
* Primarily used by overview actions to initiate split from focused task, logs the source
* of split invocation as such.
*/
- public void initiateSplitSelect(TaskView taskView) {
+ public void initiateSplitSelect(TaskContainer taskContainer) {
int defaultSplitPosition = getPagedOrientationHandler()
.getDefaultSplitPosition(mContainer.getDeviceProfile());
- initiateSplitSelect(taskView, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT);
+ initiateSplitSelect(taskContainer, defaultSplitPosition, LAUNCHER_OVERVIEW_ACTIONS_SPLIT);
}
/** TODO(b/266477929): Consolidate this call w/ the one below */
- public void initiateSplitSelect(TaskView taskView, @StagePosition int stagePosition,
+ public void initiateSplitSelect(TaskContainer taskContainer,
+ @StagePosition int stagePosition,
StatsLogManager.EventEnum splitEvent) {
+ TaskView taskView = taskContainer.getTaskView();
mSplitHiddenTaskView = taskView;
mSplitSelectStateController.setInitialTaskSelect(null /*intent*/, stagePosition,
- taskView.getFirstItemInfo(), splitEvent, taskView.getFirstTask().key.id);
+ taskContainer.getItemInfo(), splitEvent, taskContainer.getTask().key.id);
mSplitSelectStateController.setAnimateCurrentTaskDismissal(
true /*animateCurrentTaskDismissal*/);
mSplitHiddenTaskViewIndex = indexOfChild(taskView);
@@ -5273,15 +5281,16 @@
boolean isInitiatingTaskViewSplitPair =
mSplitSelectStateController.isDismissingFromSplitPair();
if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair
- && mSplitHiddenTaskView instanceof GroupedTaskView) {
+ && mSplitHiddenTaskView instanceof GroupedTaskView groupedTaskView) {
// Splitting from Overview for split pair task
createInitialSplitSelectAnimation(builder);
// Animate pair thumbnail into full thumbnail
- boolean primaryTaskSelected = mSplitHiddenTaskView.getTaskIds()[0]
+ boolean primaryTaskSelected = groupedTaskView.getLeftTopTaskContainer().getTask().key.id
== mSplitSelectStateController.getInitialTaskId();
- TaskContainer taskContainer = mSplitHiddenTaskView
- .getTaskContainers().get(primaryTaskSelected ? 1 : 0);
+ TaskContainer taskContainer =
+ primaryTaskSelected ? groupedTaskView.getRightBottomTaskContainer()
+ : groupedTaskView.getLeftTopTaskContainer();
mSplitSelectStateController.getSplitAnimationController()
.addInitialSplitFromPair(taskContainer, builder,
mContainer.getDeviceProfile(),
@@ -5767,8 +5776,12 @@
} else {
taskView.launchWithoutAnimation(this::onTaskLaunchAnimationEnd);
}
- mContainer.getStatsLogManager().logger().withItemInfo(taskView.getFirstItemInfo())
- .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
+ // TODO(b/391918297): Logging when there is no associated task.
+ ItemInfo firstItemInfo = taskView.getFirstItemInfo();
+ if (firstItemInfo != null) {
+ mContainer.getStatsLogManager().logger().withItemInfo(firstItemInfo)
+ .log(LAUNCHER_TASK_LAUNCH_SWIPE_DOWN);
+ }
} else {
onTaskLaunchAnimationEnd(false);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index bce5a5e..94e8c03 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -224,6 +224,9 @@
/** Returns true if there are at least one TaskView has been added to the RecentsView. */
fun hasTaskViews() = taskViews.any()
+ fun getTaskContainerById(taskId: Int) =
+ taskViews.firstNotNullOfOrNull { it.getTaskContainerById(taskId) }
+
private fun getRowRect(firstView: View?, lastView: View?, outRowRect: Rect) {
outRowRect.setEmpty()
firstView?.let {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index a9e84ef..0350c78 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -153,7 +153,10 @@
}
fun setState(state: TaskData?, liveTile: Boolean, hasHeader: Boolean) {
- thumbnailView.setState(TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader))
+ thumbnailView.setState(
+ TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader),
+ state?.taskId,
+ )
splitAnimationThumbnail =
if (state is TaskData.Data) state.thumbnailData?.thumbnail else null
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 20a385f..ce0efb6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -33,11 +33,9 @@
import android.view.Display
import android.view.MotionEvent
import android.view.View
-import android.view.View.OnClickListener
import android.view.ViewGroup
import android.view.ViewStub
import android.view.accessibility.AccessibilityNodeInfo
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.IntDef
@@ -64,9 +62,7 @@
import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
import com.android.launcher3.util.MultiValueAlpha
import com.android.launcher3.util.RunnableList
-import com.android.launcher3.util.SplitConfigurationOptions
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
-import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
import com.android.launcher3.util.TraceHelper
import com.android.launcher3.util.TransformingTouchDelegate
@@ -163,14 +159,15 @@
val pagedOrientationHandler: RecentsPagedOrientationHandler
get() = orientedState.orientationHandler
- @get:Deprecated("Use [taskContainers] instead.")
- val firstTask: Task
- /** Returns the first task bound to this TaskView. */
- get() = taskContainers[0].task
+ val firstTaskContainer: TaskContainer?
+ get() = taskContainers.firstOrNull()
- @get:Deprecated("Use [taskContainers] instead.")
- val firstItemInfo: ItemInfo
- get() = taskContainers[0].itemInfo
+ val firstTask: Task?
+ /** Returns the first task bound to this TaskView. */
+ get() = firstTaskContainer?.task
+
+ val firstItemInfo: ItemInfo?
+ get() = firstTaskContainer?.itemInfo
protected val container: RecentsViewContainer =
RecentsViewContainer.containerFromContext(context)
@@ -671,13 +668,6 @@
val shouldPopulateAccessibilityMenu =
modalness == 0f && recentsView?.isSplitSelectionActive == false
if (shouldPopulateAccessibilityMenu) {
- addAction(
- AccessibilityAction(
- R.id.action_close,
- context.getText(R.string.accessibility_close),
- )
- )
-
taskContainers.forEach {
TraceHelper.allowIpcs("TV.a11yInfo") {
TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut
@@ -708,11 +698,6 @@
override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
// TODO(b/343708271): Add support for multiple tasks per action.
- if (action == R.id.action_close) {
- recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/)
- return true
- }
-
taskContainers.forEach {
if (it.digitalWellBeingToast?.handleAccessibilityAction(action) == true) {
return true
@@ -942,7 +927,7 @@
protected open fun updateThumbnailSize() {
// TODO(b/271468547), we should default to setting translations only on the snapshot instead
// of a hybrid of both margins and translations
- taskContainers[0].snapshotView.updateLayoutParams<LayoutParams> {
+ firstTaskContainer?.snapshotView?.updateLayoutParams<LayoutParams> {
topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
}
taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() }
@@ -1106,10 +1091,13 @@
}
}
Log.d("b/310064698", "${taskIds.contentToString()} - onClick - callbackList: $callbackList")
- container.statsLogManager
- .logger()
- .withItemInfo(firstItemInfo)
- .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP)
+ // TODO(b/391918297): Logging when there is no associated task.
+ firstItemInfo?.let {
+ container.statsLogManager
+ .logger()
+ .withItemInfo(it)
+ .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP)
+ }
}
/** Launch of the current task (both live and inactive tasks) with an animation. */
@@ -1212,6 +1200,7 @@
* @return CompletionStage to indicate the animation completion or null if the launch failed.
*/
open fun launchAsStaticTile(): RunnableList? {
+ val firstTaskContainer = firstTaskContainer ?: return null
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN,
"startActivityFromRecentsAsync",
@@ -1223,7 +1212,7 @@
}
if (
ActivityManagerWrapper.getInstance()
- .startActivityFromRecents(taskContainers[0].task.key, opts.options)
+ .startActivityFromRecents(firstTaskContainer.task.key, opts.options)
) {
Log.d(
TAG,
@@ -1262,18 +1251,18 @@
isQuickSwitch: Boolean = false,
callback: (launched: Boolean) -> Unit,
) {
+ val firstTaskContainer = firstTaskContainer ?: return
TestLogging.recordEvent(
TestProtocol.SEQUENCE_MAIN,
"startActivityFromRecentsAsync",
taskIds.contentToString(),
)
- val firstContainer = taskContainers[0]
val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext)
if (isQuickSwitch) {
// We only listen for failures to launch in quickswitch because the during this
// gesture launcher is in the background state, vs other launches which are in
// the actual overview state
- failureListener.register(container, firstContainer.task.key.id) {
+ failureListener.register(container, firstTaskContainer.task.key.id) {
notifyTaskLaunchFailed("launchWithoutAnimation")
recentsView?.let {
// Disable animations for now, as it is an edge case and the app usually
@@ -1305,12 +1294,12 @@
if (isQuickSwitch) {
setFreezeRecentTasksReordering()
}
- disableStartingWindow = firstContainer.shouldShowSplashView
+ disableStartingWindow = firstTaskContainer.shouldShowSplashView
}
Executors.UI_HELPER_EXECUTOR.execute {
if (
!ActivityManagerWrapper.getInstance()
- .startActivityFromRecents(firstContainer.task.key, opts)
+ .startActivityFromRecents(firstTaskContainer.task.key, opts)
) {
// If the call to start activity failed, then post the result immediately,
// otherwise, wait for the animation start callback from the activity options
@@ -1337,14 +1326,6 @@
Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show()
}
- fun initiateSplitSelect(splitPositionOption: SplitPositionOption) {
- recentsView?.initiateSplitSelect(
- this,
- splitPositionOption.stagePosition,
- SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition),
- )
- }
-
/**
* Returns `true` if user is already in split select mode and this tap was to choose the second
* app. `false` otherwise
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
index e150568..90c9553 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRule.kt
@@ -125,7 +125,8 @@
// Needs to be set on window context instead of sandbox context, because it does
// does not propagate between them. However, this change will impact created
// TaskbarActivityContext instances, since they wrap the window context.
- taskbarManager.windowContext.resources.configuration.setLayoutDirection(
+ // TODO: iterate through all window contexts and do this.
+ taskbarManager.primaryWindowContext.resources.configuration.setLayoutDirection(
RTL_LOCALE
)
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt
new file mode 100644
index 0000000..9b0a95a
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/widget/picker/WidgetCategoryFilterTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker
+
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD
+import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class WidgetCategoryFilterTest {
+
+ @Test
+ fun filterValueZero_everythingMatches() {
+ val noFilter = WidgetCategoryFilter(categoryMask = 0)
+
+ noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ noFilter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ noFilter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD)
+ noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+ noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD)
+ noFilter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_NOT_KEYGUARD)
+ noFilter.assertMatches(
+ WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD
+ )
+ noFilter.assertMatches(
+ WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_NOT_KEYGUARD
+ )
+ noFilter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+ }
+
+ @Test
+ fun includeHomeScreen_matchesOnlyIfHomeScreenExists() {
+ val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN)
+
+ filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_SEARCHBOX)
+ }
+
+ @Test
+ fun includeHomeScreenOrKeyguard_matchesIfEitherHomeScreenOrKeyguardExists() {
+ val filter = WidgetCategoryFilter(WIDGET_CATEGORY_HOME_SCREEN or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+ }
+
+ @Test
+ fun excludeNotKeyguard_doesNotMatchIfNotKeyguardExists() {
+ val filter = WidgetCategoryFilter(WIDGET_CATEGORY_NOT_KEYGUARD.inv())
+
+ filter.assertMatches(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(
+ WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN
+ )
+ }
+
+ @Test
+ fun multipleExclusions_doesNotMatchIfExcludedCategoriesExist() {
+ val filter =
+ WidgetCategoryFilter(
+ WIDGET_CATEGORY_HOME_SCREEN.inv() and WIDGET_CATEGORY_NOT_KEYGUARD.inv()
+ )
+
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX)
+ filter.assertMatches(WIDGET_CATEGORY_KEYGUARD)
+ filter.assertMatches(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_KEYGUARD)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_HOME_SCREEN)
+
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD)
+ filter.assertDoesNotMatch(WIDGET_CATEGORY_SEARCHBOX or WIDGET_CATEGORY_NOT_KEYGUARD)
+ filter.assertDoesNotMatch(
+ WIDGET_CATEGORY_NOT_KEYGUARD or WIDGET_CATEGORY_KEYGUARD or WIDGET_CATEGORY_HOME_SCREEN
+ )
+ }
+
+ private fun WidgetCategoryFilter.assertMatches(category: Int) {
+ assertThat(matches(category)).isTrue()
+ }
+
+ private fun WidgetCategoryFilter.assertDoesNotMatch(category: Int) {
+ assertThat(matches(category)).isFalse()
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt
new file mode 100644
index 0000000..b4c236e
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/ActiveTrackpadListTest.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2025 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.util
+
+import android.hardware.input.InputManager
+import android.view.InputDevice
+import android.view.InputDevice.SOURCE_MOUSE
+import android.view.InputDevice.SOURCE_TOUCHPAD
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.SandboxApplication
+import com.android.launcher3.util.TestUtil
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class ActiveTrackpadListTest {
+
+ @get:Rule val context = SandboxApplication()
+
+ private val inputDeviceIds = IntArray()
+ private lateinit var inputManager: InputManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ inputManager = context.spyService(InputManager::class.java)
+ doAnswer { inputDeviceIds.toArray() }.whenever(inputManager).inputDeviceIds
+
+ doReturn(null).whenever(inputManager).getInputDevice(eq(1))
+ doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD))
+ .whenever(inputManager)
+ .getInputDevice(eq(2))
+ doReturn(mockDevice(SOURCE_MOUSE or SOURCE_TOUCHPAD))
+ .whenever(inputManager)
+ .getInputDevice(eq(3))
+ doReturn(mockDevice(SOURCE_MOUSE)).whenever(inputManager).getInputDevice(eq(4))
+ }
+
+ @Test
+ fun `initialize correct devices`() {
+ inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4))
+
+ val list = ActiveTrackpadList(context) {}
+ assertEquals(2, list.size())
+ assertTrue(list.contains(2))
+ assertTrue(list.contains(3))
+ }
+
+ @Test
+ fun `update callback not called in constructor`() {
+ inputDeviceIds.addAll(IntArray.wrap(2, 3))
+
+ var updateCalled = false
+ val list = ActiveTrackpadList(context) { updateCalled = true }
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+
+ assertEquals(2, list.size())
+ assertFalse(updateCalled)
+ }
+
+ @Test
+ fun `update called on add only once`() {
+ var updateCalled = false
+ val list = ActiveTrackpadList(context) { updateCalled = true }
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+
+ assertFalse(updateCalled)
+ assertEquals(0, list.size())
+
+ list.onInputDeviceAdded(1)
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+ assertFalse(updateCalled)
+ assertEquals(0, list.size())
+
+ list.onInputDeviceAdded(2)
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+ assertTrue(updateCalled)
+ assertEquals(1, list.size())
+
+ updateCalled = false
+ list.onInputDeviceAdded(3)
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+ assertFalse(updateCalled)
+ assertEquals(2, list.size())
+ }
+
+ @Test
+ fun `update called on remove only once`() {
+ var updateCalled = false
+ inputDeviceIds.addAll(IntArray.wrap(1, 2, 3, 4))
+ val list = ActiveTrackpadList(context) { updateCalled = true }
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+ assertEquals(2, list.size())
+
+ list.onInputDeviceRemoved(2)
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+ assertEquals(1, list.size())
+ assertFalse(updateCalled)
+
+ list.onInputDeviceRemoved(3)
+ TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {}
+ assertEquals(0, list.size())
+ assertTrue(updateCalled)
+ }
+
+ private fun mockDevice(sources: Int) =
+ mock(InputDevice::class.java).apply { doReturn(sources).whenever(this).sources }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 0491c07..e4bdba5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -102,12 +102,12 @@
fun activeTasks_noMatchingTasks() {
val nonMatchingComponent = ComponentKey(ComponentName("no", "match"), primaryUserHandle)
val groupTask1 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pomegranate", "juice"),
ComponentName("pumpkin", "pie"),
)
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("hotdog", "juice"),
ComponentName("personal", "computer"),
)
@@ -143,12 +143,12 @@
val matchingComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
- generateGroupTask(
+ generateSplitTask(
ComponentName(matchingPackage, matchingClass),
ComponentName("pomegranate", "juice"),
)
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pumpkin", "pie"),
ComponentName("personal", "computer"),
)
@@ -170,7 +170,7 @@
it[0].key.baseIntent.component?.className,
matchingClass,
)
- assertEquals(it[0], groupTask1.task1)
+ assertEquals(it[0], groupTask1.topLeftTask)
}
// Capture callback from recentsModel#getTasks()
@@ -196,12 +196,12 @@
val nonPrimaryUserComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle)
val groupTask1 =
- generateGroupTask(
+ generateSplitTask(
ComponentName(matchingPackage, matchingClass),
ComponentName("pomegranate", "juice"),
)
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pumpkin", "pie"),
ComponentName("personal", "computer"),
)
@@ -237,14 +237,14 @@
val nonPrimaryUserComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), nonPrimaryUserHandle)
val groupTask1 =
- generateGroupTask(
+ generateSplitTask(
ComponentName(matchingPackage, matchingClass),
nonPrimaryUserHandle,
ComponentName("pomegranate", "juice"),
nonPrimaryUserHandle,
)
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pumpkin", "pie"),
ComponentName("personal", "computer"),
)
@@ -267,7 +267,7 @@
matchingClass,
)
assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
- assertEquals(it[0], groupTask1.task1)
+ assertEquals(it[0], groupTask1.topLeftTask)
}
// Capture callback from recentsModel#getTasks()
@@ -293,12 +293,12 @@
val matchingComponent =
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
- generateGroupTask(
+ generateSplitTask(
ComponentName(matchingPackage, matchingClass),
ComponentName("pumpkin", "pie"),
)
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass),
)
@@ -320,7 +320,7 @@
it[0].key.baseIntent.component?.className,
matchingClass,
)
- assertEquals(it[0], groupTask1.task1)
+ assertEquals(it[0], groupTask1.topLeftTask)
}
// Capture callback from recentsModel#getTasks()
@@ -348,9 +348,9 @@
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
- generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+ generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass),
)
@@ -374,7 +374,7 @@
it[1].key.baseIntent.component?.className,
matchingClass,
)
- assertEquals(it[1], groupTask2.task2)
+ assertEquals(it[1], groupTask2.bottomRightTask)
}
// Capture callback from recentsModel#getTasks()
@@ -401,9 +401,9 @@
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
- generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+ generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass),
)
@@ -426,7 +426,7 @@
it[0].key.baseIntent.component?.className,
matchingClass,
)
- assertEquals(it[0], groupTask2.task2)
+ assertEquals(it[0], groupTask2.bottomRightTask)
assertNull("No tasks should have matched", it[1] /*task*/)
}
@@ -454,12 +454,12 @@
ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
val groupTask1 =
- generateGroupTask(
+ generateSplitTask(
ComponentName(matchingPackage, matchingClass),
ComponentName("pumpkin", "pie"),
)
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("pomegranate", "juice"),
ComponentName(matchingPackage, matchingClass),
)
@@ -482,7 +482,7 @@
it[0].key.baseIntent.component?.className,
matchingClass,
)
- assertEquals(it[0], groupTask1.task1)
+ assertEquals(it[0], groupTask1.topLeftTask)
assertEquals(
"ComponentName package mismatched",
it[1].key.baseIntent.component?.packageName,
@@ -493,7 +493,7 @@
it[1].key.baseIntent.component?.className,
matchingClass,
)
- assertEquals(it[1], groupTask2.task2)
+ assertEquals(it[1], groupTask2.bottomRightTask)
}
// Capture callback from recentsModel#getTasks()
@@ -524,14 +524,14 @@
ComponentKey(ComponentName(matchingPackage2, matchingClass2), primaryUserHandle)
val groupTask1 =
- generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+ generateSplitTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
val groupTask2 =
- generateGroupTask(
+ generateSplitTask(
ComponentName(matchingPackage2, matchingClass2),
ComponentName(matchingPackage, matchingClass),
)
val groupTask3 =
- generateGroupTask(
+ generateSplitTask(
ComponentName("hotdog", "pie"),
ComponentName(matchingPackage, matchingClass),
)
@@ -545,7 +545,7 @@
val taskConsumer =
Consumer<Array<Task>> {
assertEquals("Expected array length 2", 2, it.size)
- assertEquals("Found wrong task", it[0], groupTask2.task1)
+ assertEquals("Found wrong task", it[0], groupTask2.topLeftTask)
}
// Capture callback from recentsModel#getTasks()
@@ -640,11 +640,11 @@
verify(recentsView, times(0)).resetDesktopTaskFromSplitSelectState()
}
- // Generate GroupTask with default userId.
- private fun generateGroupTask(
+ /** Generates a [SplitTask] with default userId. */
+ private fun generateSplitTask(
task1ComponentName: ComponentName,
task2ComponentName: ComponentName,
- ): GroupTask {
+ ): SplitTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
taskInfo.taskId = getUniqueId()
@@ -666,20 +666,20 @@
SplitConfigurationOptions.SplitBounds(
/* leftTopBounds = */ Rect(),
/* rightBottomBounds = */ Rect(),
- /* leftTopTaskId = */ -1,
- /* rightBottomTaskId = */ -1,
+ /* leftTopTaskId = */ task1.key.id,
+ /* rightBottomTaskId = */ task2.key.id,
/* snapPosition = */ SNAP_TO_2_50_50,
),
)
}
- // Generate GroupTask with custom user handles.
- private fun generateGroupTask(
+ /** Generates a [SplitTask] with custom user handles. */
+ private fun generateSplitTask(
task1ComponentName: ComponentName,
userHandle1: UserHandle,
task2ComponentName: ComponentName,
userHandle2: UserHandle,
- ): GroupTask {
+ ): SplitTask {
val task1 = Task()
var taskInfo = ActivityManager.RunningTaskInfo()
taskInfo.taskId = getUniqueId()
@@ -704,8 +704,8 @@
SplitConfigurationOptions.SplitBounds(
/* leftTopBounds = */ Rect(),
/* rightBottomBounds = */ Rect(),
- /* leftTopTaskId = */ -1,
- /* rightBottomTaskId = */ -1,
+ /* leftTopTaskId = */ task1.key.id,
+ /* rightBottomTaskId = */ task2.key.id,
/* snapPosition = */ SNAP_TO_2_50_50,
),
)
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
index f923142..c78fe1c 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.app.PendingIntent;
@@ -93,7 +94,8 @@
final TaskView task = getOnceNotNull("No latest task", launcher -> getLatestTask(launcher));
return getFromLauncher(launcher -> {
- TaskContainer taskContainer = task.getTaskContainers().get(0);
+ TaskContainer taskContainer = task.getFirstTaskContainer();
+ assertNotNull(taskContainer);
assertTrue("Latest task is not Calculator", calculatorPackage.equals(
taskContainer.getTask().getTopComponent().getPackageName()));
return taskContainer.getDigitalWellBeingToast();
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index a925866..0b8e52a 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -38,13 +38,13 @@
<string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"لا يمكن استخدام هذين التطبيقَين في الوقت نفسه على هذا الجهاز"</string>
<string name="app_pair_needs_unfold" msgid="4588897528143807002">"افتح الجهاز لاستخدام هذين التطبيقَين في الوقت نفسه"</string>
<string name="app_pair_not_available" msgid="3556767440808032031">"ميزة \"استخدام تطبيقين في الوقت نفسه\" غير متوفّرة"</string>
- <string name="long_press_widget_to_add" msgid="3587712543577675817">"انقر مع الاستمرار لنقل أداة."</string>
- <string name="long_accessible_way_to_add" msgid="2733588281439571974">"انقر مرتين مع تثبيت إصبعك لنقل أداة أو استخدام الإجراءات المخصّصة."</string>
+ <string name="long_press_widget_to_add" msgid="3587712543577675817">"انقر مع الاستمرار لنقل تطبيق مصغَّر."</string>
+ <string name="long_accessible_way_to_add" msgid="2733588281439571974">"انقر مرتين مع تثبيت إصبعك لنقل تطبيق مصغَّر أو استخدام الإجراءات المخصّصة."</string>
<string name="widget_picker_widget_options_button_description" msgid="4770099264476852363">"خيارات إضافية"</string>
<string name="widget_picker_show_all_widgets_menu_item_title" msgid="9023638224586908119">"عرض كل التطبيقات المصغّرة"</string>
<string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
<string name="widget_accessible_dims_format" msgid="3640149169885301790">"العرض %1$d الطول %2$d"</string>
- <string name="widget_preview_context_description" msgid="9045841361655787574">"أداة <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
+ <string name="widget_preview_context_description" msgid="9045841361655787574">"التطبيق المصغَّر <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"التطبيق المصغّرة \"<xliff:g id="WIDGET_NAME">%1$s</xliff:g>\"، بعرض %2$d وارتفاع %3$d"</string>
<string name="add_item_request_drag_hint" msgid="8730547755622776606">"يُرجى النقر مع الاستمرار على التطبيق المصغّر لنقله إلى الشاشة الرئيسية"</string>
<string name="add_to_home_screen" msgid="9168649446635919791">"إضافة إلى الشاشة الرئيسية"</string>
@@ -75,8 +75,8 @@
<string name="widgets_list_expand_button_label" msgid="7912016136574932622">"عرض الكل"</string>
<string name="widgets_list_expand_button_content_description" msgid="4600513860973450888">"عرض كل التطبيقات المصغّرة"</string>
<string name="widgets_list_expanded" msgid="7374857868788557730">"جارٍ عرض كل التطبيقات المصغّرة"</string>
- <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"انقر لتغيير إعدادات الأداة"</string>
- <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغيير إعدادات الأداة"</string>
+ <string name="reconfigurable_widget_education_tip" msgid="6336962690888067057">"انقر لتغيير إعدادات التطبيق المصغَّر"</string>
+ <string name="widget_reconfigure_button_content_description" msgid="8811472721881205250">"تغيير إعدادات التطبيق المصغَّر"</string>
<string name="all_apps_search_bar_hint" msgid="1390553134053255246">"بحث في التطبيقات"</string>
<string name="all_apps_loading_message" msgid="5813968043155271636">"جارٍ تحميل التطبيقات…"</string>
<string name="all_apps_no_search_results" msgid="3200346862396363786">"لم يتم العثور على أي تطبيقات تتطابق مع \"<xliff:g id="QUERY">%1$s</xliff:g>\""</string>
@@ -108,7 +108,7 @@
<string name="permdesc_read_settings" msgid="4208061150510996676">"يسمح هذا الإذن للتطبيق بالاطلاع على الإعدادات والاختصارات على الشاشة الرئيسية."</string>
<string name="permlab_write_settings" msgid="4820028712156303762">"تعديل الإعدادات والاختصارات على الشاشة الرئيسية"</string>
<string name="permdesc_write_settings" msgid="726859348127868466">"يسمح هذا الإذن للتطبيق بتغيير الإعدادات والاختصارات على الشاشة الرئيسية."</string>
- <string name="gadget_error_text" msgid="740356548025791839">"يتعذّر تحميل الأداة."</string>
+ <string name="gadget_error_text" msgid="740356548025791839">"يتعذّر تحميل التطبيق المصغَّر."</string>
<string name="gadget_setup_text" msgid="8348374825537681407">"إعدادات التطبيق المصغّر"</string>
<string name="gadget_complete_setup_text" msgid="309040266978007925">"انقر لإكمال الإعداد."</string>
<string name="uninstall_system_app_text" msgid="4172046090762920660">"هذا تطبيق نظام وتتعذر إزالته."</string>
@@ -181,7 +181,7 @@
<string name="action_increase_height" msgid="459390020612501122">"زيادة الارتفاع"</string>
<string name="action_decrease_width" msgid="1374549771083094654">"تقليل العرض"</string>
<string name="action_decrease_height" msgid="282377193880900022">"تقليل الارتفاع"</string>
- <string name="widget_resized" msgid="9130327887929620">"تم تغيير حجم الأداة إلى العرض <xliff:g id="NUMBER_0">%1$s</xliff:g> والارتفاع <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
+ <string name="widget_resized" msgid="9130327887929620">"تم تغيير حجم التطبيق المصغَّر إلى العرض <xliff:g id="NUMBER_0">%1$s</xliff:g> والارتفاع <xliff:g id="NUMBER_1">%2$s</xliff:g>"</string>
<string name="action_deep_shortcut" msgid="4766835855579976045">"قائمة الاختصارات"</string>
<string name="action_dismiss_notification" msgid="5909461085055959187">"تجاهل"</string>
<string name="accessibility_close" msgid="2277148124685870734">"إغلاق"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 754bd76..1dad001 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -46,7 +46,7 @@
<string name="widget_accessible_dims_format" msgid="3640149169885301790">"Szerokość %1$d, wysokość %2$d"</string>
<string name="widget_preview_context_description" msgid="9045841361655787574">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>"</string>
<string name="widget_preview_name_and_dims_content_description" msgid="8489038126122831595">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g>, %2$d (szerokość), %3$d (wysokość)"</string>
- <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Aby poruszać widżetem po ekranie głównym, kliknij go i przytrzymaj"</string>
+ <string name="add_item_request_drag_hint" msgid="8730547755622776606">"Aby przesunąć widżet na ekranie głównym, kliknij go i przytrzymaj"</string>
<string name="add_to_home_screen" msgid="9168649446635919791">"Dodaj do ekranu głównego"</string>
<string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"Widżet <xliff:g id="WIDGET_NAME">%1$s</xliff:g> został dodany do ekranu głównego"</string>
<string name="suggested_widgets_header_title" msgid="1844314680798145222">"Sugestie"</string>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index e06895c..f740489 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -174,7 +174,8 @@
<declare-styleable name="GridDisplayOption">
<attr name="name" format="string" />
- <attr name="title" />
+ <attr name="gridTitle" format="string" />
+ <attr name="gridIconId" format="reference"/>
<attr name="numRows" format="integer" />
<attr name="numColumns" format="integer" />
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index b4a24f1..813d8f1 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -25,7 +25,6 @@
import static com.android.launcher3.InvariantDeviceProfile.INDEX_TWO_PANEL_PORTRAIT;
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.Utilities.pxFromSp;
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.testing.shared.ResourceUtils.pxFromDp;
@@ -52,6 +51,7 @@
import com.android.launcher3.CellLayout.ContainerType;
import com.android.launcher3.DevicePaddings.DevicePadding;
+import com.android.launcher3.folder.ClippedFolderIconLayoutRule;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
@@ -1228,7 +1228,7 @@
}
private int getIconSizeWithOverlap(int iconSize) {
- return (int) Math.ceil(iconSize * ICON_OVERLAP_FACTOR);
+ return (int) Math.ceil(iconSize * ClippedFolderIconLayoutRule.getIconOverlapFactor());
}
/**
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 753e017..e47a44a 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -993,7 +993,8 @@
private static final int DONT_INLINE_QSB = 0;
public final String name;
- public final String title;
+ public final String gridTitle;
+ public final int gridIconId;
public final int numRows;
public final int numColumns;
public final int numSearchContainerColumns;
@@ -1042,7 +1043,9 @@
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.GridDisplayOption);
name = a.getString(R.styleable.GridDisplayOption_name);
- title = a.getString(R.styleable.GridDisplayOption_title);
+ gridTitle = a.getString(R.styleable.GridDisplayOption_gridTitle);
+ gridIconId = a.getResourceId(
+ R.styleable.GridDisplayOption_gridIconId, INVALID_RESOURCE_HANDLE);
deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
DEVICE_CATEGORY_ALL);
mGridSizeSpecsId = a.getResourceId(
diff --git a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
index 8cd91d3..cf5150a 100644
--- a/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
+++ b/src/com/android/launcher3/folder/ClippedFolderIconLayoutRule.java
@@ -1,5 +1,7 @@
package com.android.launcher3.folder;
+import com.android.launcher3.Flags;
+
public class ClippedFolderIconLayoutRule {
public static final int MAX_NUM_ITEMS_IN_PREVIEW = 4;
@@ -7,9 +9,12 @@
private static final float MIN_SCALE = 0.44f;
private static final float MAX_SCALE = 0.51f;
+ // TODO: figure out exact radius for different icons
+ private static final float MAX_RADIUS_DILATION_SHAPES = 0.15f;
private static final float MAX_RADIUS_DILATION = 0.25f;
// The max amount of overlap the preview items can go outside of the background bounds.
public static final float ICON_OVERLAP_FACTOR = 1 + (MAX_RADIUS_DILATION / 2f);
+ public static final float ICON_OVERLAP_FACTOR_SHAPES = 1f;
private static final float ITEM_RADIUS_SCALE_FACTOR = 1.15f;
public static final int EXIT_INDEX = -2;
@@ -28,7 +33,7 @@
mRadius = ITEM_RADIUS_SCALE_FACTOR * availableSpace / 2f;
mIconSize = intrinsicIconSize;
mIsRtl = rtl;
- mBaselineIconScale = availableSpace / (intrinsicIconSize * 1f);
+ mBaselineIconScale = availableSpace / intrinsicIconSize;
}
public PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems,
@@ -84,6 +89,7 @@
result[1] = top + (row * dy);
}
+ // b/392610664 TODO: Change positioning from circular geometry to square / grid-based.
private void getPosition(int index, int curNumItems, float[] result) {
// The case of two items is homomorphic to the case of one.
curNumItems = Math.max(curNumItems, 2);
@@ -113,8 +119,10 @@
}
// We bump the radius up between 0 and MAX_RADIUS_DILATION % as the number of items increase
- float radius = mRadius * (1 + MAX_RADIUS_DILATION * (curNumItems -
- MIN_NUM_ITEMS_IN_PREVIEW) / (MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
+ float radiusDilation = Flags.enableLauncherIconShapes() ? MAX_RADIUS_DILATION_SHAPES
+ : MAX_RADIUS_DILATION;
+ float radius = mRadius * (1 + radiusDilation * (curNumItems - MIN_NUM_ITEMS_IN_PREVIEW)
+ / (MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
double theta = theta0 + index * (2 * Math.PI / curNumItems) * direction;
float halfIconSize = (mIconSize * scaleForItem(curNumItems)) / 2;
@@ -130,7 +138,7 @@
public float scaleForItem(int numItems) {
// Scale is determined by the number of items in the preview.
final float scale;
- if (numItems <= 3) {
+ if (numItems <= 3 && !Flags.enableLauncherIconShapes()) {
scale = MAX_SCALE;
} else {
scale = MIN_SCALE;
@@ -141,4 +149,15 @@
public float getIconSize() {
return mIconSize;
}
+
+ /**
+ * Gets correct constant for icon overlap.
+ */
+ public static float getIconOverlapFactor() {
+ if (Flags.enableLauncherIconShapes()) {
+ return ICON_OVERLAP_FACTOR_SHAPES;
+ } else {
+ return ICON_OVERLAP_FACTOR;
+ }
+ }
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index b76e098..fb48a4d 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -1708,7 +1708,7 @@
@Override
public boolean canInterceptEventsInSystemGestureRegion() {
- return true;
+ return !mIsEditingName;
}
/**
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index b7b378e..0ed8787 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -17,7 +17,6 @@
package com.android.launcher3.folder;
import static com.android.launcher3.Flags.enableCursorHoverStates;
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW;
import static com.android.launcher3.folder.FolderGridOrganizer.createFolderGridOrganizer;
import static com.android.launcher3.folder.PreviewItemManager.INITIAL_ITEM_ANIMATION_DURATION;
@@ -248,7 +247,8 @@
mPreviewItemManager.recomputePreviewDrawingParams();
mBackground.getBounds(outBounds);
// The preview items go outside of the bounds of the background.
- Utilities.scaleRectAboutCenter(outBounds, ICON_OVERLAP_FACTOR);
+ Utilities.scaleRectAboutCenter(outBounds,
+ ClippedFolderIconLayoutRule.getIconOverlapFactor());
}
public float getBackgroundStrokeWidth() {
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index d9c60db..77fa355 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -18,7 +18,6 @@
import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
-import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ICON_OVERLAP_FACTOR;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import android.animation.Animator;
@@ -373,7 +372,7 @@
public Path getClipPath() {
mPath.reset();
- float radius = getScaledRadius() * ICON_OVERLAP_FACTOR;
+ float radius = getScaledRadius() * ClippedFolderIconLayoutRule.getIconOverlapFactor();
// Find the difference in radius so that the clip path remains centered.
float radiusDifference = radius - getRadius();
float offsetX = basePreviewOffsetX - radiusDifference;
diff --git a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
index 5a2864d..9edc386 100644
--- a/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
+++ b/src/com/android/launcher3/graphics/GridCustomizationsProvider.java
@@ -47,7 +47,7 @@
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.shapes.IconShapeModel;
-import com.android.launcher3.shapes.IconShapesProvider;
+import com.android.launcher3.shapes.ShapesProvider;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
@@ -86,10 +86,13 @@
private static final String TAG = "GridCustomizationsProvider";
+ // KEY_NAME is the name of the grid used internally while the KEY_GRID_TITLE is the translated
+ // string title of the grid.
private static final String KEY_NAME = "name";
private static final String KEY_GRID_TITLE = "grid_title";
private static final String KEY_ROWS = "rows";
private static final String KEY_COLS = "cols";
+ private static final String KEY_GRID_ICON_ID = "grid_icon_id";
private static final String KEY_PREVIEW_COUNT = "preview_count";
// is_default means if a certain option is currently set to the system
private static final String KEY_IS_DEFAULT = "is_default";
@@ -145,7 +148,7 @@
KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT});
String currentShapePath =
ThemeManager.INSTANCE.get(context).getIconState().getIconMask();
- for (IconShapeModel shape : IconShapesProvider.INSTANCE.getShapes().values()) {
+ for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes().values()) {
cursor.newRow()
.add(KEY_SHAPE_KEY, shape.getKey())
.add(KEY_SHAPE_TITLE, shape.getTitle())
@@ -161,17 +164,18 @@
case KEY_LIST_OPTIONS: {
MatrixCursor cursor = new MatrixCursor(new String[]{
KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT,
- KEY_IS_DEFAULT});
+ KEY_IS_DEFAULT, KEY_GRID_ICON_ID});
InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext());
for (GridOption gridOption : idp.parseAllGridOptions(getContext())) {
cursor.newRow()
.add(KEY_NAME, gridOption.name)
- .add(KEY_GRID_TITLE, gridOption.title)
+ .add(KEY_GRID_TITLE, gridOption.gridTitle)
.add(KEY_ROWS, gridOption.numRows)
.add(KEY_COLS, gridOption.numColumns)
.add(KEY_PREVIEW_COUNT, 1)
.add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns
- && idp.numRows == gridOption.numRows);
+ && idp.numRows == gridOption.numRows)
+ .add(KEY_GRID_ICON_ID, gridOption.gridIconId);
}
return cursor;
}
diff --git a/src/com/android/launcher3/graphics/ThemeManager.kt b/src/com/android/launcher3/graphics/ThemeManager.kt
index f24c2ab..9f35e4a 100644
--- a/src/com/android/launcher3/graphics/ThemeManager.kt
+++ b/src/com/android/launcher3/graphics/ThemeManager.kt
@@ -27,7 +27,7 @@
import com.android.launcher3.dagger.LauncherAppSingleton
import com.android.launcher3.icons.IconThemeController
import com.android.launcher3.icons.mono.MonoIconThemeController
-import com.android.launcher3.shapes.IconShapesProvider
+import com.android.launcher3.shapes.ShapesProvider
import com.android.launcher3.util.DaggerSingletonObject
import com.android.launcher3.util.DaggerSingletonTracker
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
@@ -94,7 +94,7 @@
private fun parseIconState(): IconState {
val shapeModel =
prefs.get(PREF_ICON_SHAPE).let { shapeOverride ->
- IconShapesProvider.shapes.values.firstOrNull { it.key == shapeOverride }
+ ShapesProvider.iconShapes.values.firstOrNull { it.key == shapeOverride }
}
val iconMask =
when {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 6eb02ab..2d30466 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -225,9 +225,12 @@
@UiEvent(doc = "User tapped on desktop icon on a task menu.")
LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP(1706),
- @UiEvent(doc = "Use tapped on external display icon on a task menu,")
+ @UiEvent(doc = "User tapped on external display icon on a task menu,")
LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP(1957),
+ @UiEvent(doc = "User tapped on close app on a task menu,")
+ LAUNCHER_SYSTEM_SHORTCUT_CLOSE_APP_TAP(2081),
+
@UiEvent(doc = "User tapped on pause app system shortcut.")
LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521),
diff --git a/src/com/android/launcher3/model/GridSizeMigrationDBController.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
index d256d1b..b291421 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationDBController.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java
@@ -17,7 +17,6 @@
package com.android.launcher3.model;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.Flags.oneGridSpecs;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
@@ -39,6 +38,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.LauncherSettings;
@@ -127,7 +127,7 @@
return true;
}
- boolean shouldMigrateToStrictlyTallerGrid = isDestNewDb
+ boolean shouldMigrateToStrictlyTallerGrid = (Flags.oneGridSpecs() || isDestNewDb)
&& srcDeviceState.getColumns().equals(destDeviceState.getColumns())
&& srcDeviceState.getRows() < destDeviceState.getRows();
if (shouldMigrateToStrictlyTallerGrid) {
@@ -142,7 +142,7 @@
if (shouldMigrateToStrictlyTallerGrid) {
// We want to add the extra row(s) to the top of the screen, so we shift the grid
// down.
- if (oneGridSpecs()) {
+ if (Flags.oneGridSpecs()) {
shiftWorkspaceByXCells(
target.getWritableDatabase(),
(destDeviceState.getRows() - srcDeviceState.getRows()),
diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
index 876919a..9586bf3 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
+++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt
@@ -21,7 +21,6 @@
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags
-import com.android.launcher3.Flags.oneGridSpecs
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.get
import com.android.launcher3.LauncherPrefs.Companion.getPrefs
@@ -81,7 +80,7 @@
// down.
if (shouldMigrateToStrtictlyTallerGrid) {
Log.d(TAG, "Migrating to strictly taller grid")
- if (oneGridSpecs()) {
+ if (Flags.oneGridSpecs()) {
shiftWorkspaceByXCells(
target.writableDatabase,
(destDeviceState.rows - srcDeviceState.rows),
@@ -374,7 +373,7 @@
srcDeviceState: DeviceGridState,
destDeviceState: DeviceGridState,
): Boolean {
- return isDestNewDb &&
+ return (Flags.oneGridSpecs() || isDestNewDb) &&
srcDeviceState.columns == destDeviceState.columns &&
srcDeviceState.rows < destDeviceState.rows
}
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 1623881..6a8d86b 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -42,6 +42,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings.Favorites;
@@ -201,7 +202,7 @@
info.itemType = itemType;
info.title = getTitle();
// the fallback icon
- if (!loadIcon(info)) {
+ if (!loadIconFromDb(info)) {
info.bitmap = mIconCache.getDefaultIcon(info.user);
}
@@ -213,15 +214,15 @@
/**
* Loads the icon from the cursor and updates the {@param info} if the icon is an app resource.
*/
- protected boolean loadIcon(WorkspaceItemInfo info) {
- return createIconRequestInfo(info, false).loadWorkspaceIcon(mContext);
+ protected boolean loadIconFromDb(WorkspaceItemInfo info) {
+ return createIconRequestInfo(info, false).loadIconFromDbBlob(mContext);
}
public IconRequestInfo<WorkspaceItemInfo> createIconRequestInfo(
WorkspaceItemInfo wai, boolean useLowResIcon) {
byte[] iconBlob = itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT || restoreFlag != 0
+ || (wai.isInactiveArchive() && Flags.restoreArchivedAppIconsFromDb())
? getIconBlob() : null;
-
return new IconRequestInfo<>(wai, mActivityInfo, iconBlob, useLowResIcon);
}
@@ -312,7 +313,7 @@
info.intent = intent;
// the fallback icon
- if (!loadIcon(info)) {
+ if (!loadIconFromDb(info)) {
mIconCache.getTitleAndIcon(info, DEFAULT_LOOKUP_FLAG);
}
@@ -375,20 +376,11 @@
info.intent = newIntent;
UserCache userCache = UserCache.getInstance(mContext);
UserIconInfo userIconInfo = userCache.getUserInfo(user);
-
- if (loadIcon) {
- mIconCache.getTitleAndIcon(info, mActivityInfo,
- DEFAULT_LOOKUP_FLAG.withUseLowRes(useLowResIcon));
- if (mIconCache.isDefaultIcon(info.bitmap, user)) {
- loadIcon(info);
- }
- }
-
if (mActivityInfo != null) {
AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo,
ApiWrapper.INSTANCE.get(mContext), mPmHelper);
}
-
+ loadWorkspaceTitleAndIcon(useLowResIcon, loadIcon, info);
// from the db
if (TextUtils.isEmpty(info.title)) {
if (loadIcon) {
@@ -407,6 +399,32 @@
return info;
}
+ @VisibleForTesting
+ void loadWorkspaceTitleAndIcon(
+ boolean useLowResIcon,
+ boolean loadIconFromCache,
+ WorkspaceItemInfo info
+ ) {
+ boolean isPreArchived = Flags.enableSupportForArchiving()
+ && Flags.restoreArchivedAppIconsFromDb()
+ && info.isInactiveArchive();
+ boolean preArchivedIconNotFound = isPreArchived && !loadIconFromDb(info);
+ if (preArchivedIconNotFound) {
+ Log.d(TAG, "loadIconFromDb failed for pre-archived icon, loading from cache."
+ + " Component=" + info.getTargetComponent());
+ mIconCache.getTitleAndIcon(info, mActivityInfo,
+ DEFAULT_LOOKUP_FLAG.withUseLowRes(useLowResIcon));
+ } else if (loadIconFromCache && !info.isInactiveArchive()) {
+ mIconCache.getTitleAndIcon(info, mActivityInfo,
+ DEFAULT_LOOKUP_FLAG.withUseLowRes(useLowResIcon));
+ if (mIconCache.isDefaultIcon(info.bitmap, user)) {
+ Log.d(TAG, "Default Icon found in cache, trying DB instead. "
+ + " Component=" + info.getTargetComponent());
+ loadIconFromDb(info);
+ }
+ }
+ }
+
/**
* Returns a {@link ContentWriter} which can be used to update the current item.
*/
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index fee9696..3ee029b 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -26,6 +26,7 @@
import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.icons.CacheableShortcutInfo.convertShortcutsToCacheableShortcuts;
+import static com.android.launcher3.icons.cache.CacheLookupFlag.DEFAULT_LOOKUP_FLAG;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
@@ -106,11 +107,13 @@
import com.android.launcher3.widget.WidgetInflater;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
@@ -154,6 +157,8 @@
private Map<ShortcutKey, ShortcutInfo> mShortcutKeyToPinnedShortcuts;
private HashMap<PackageUserKey, SessionInfo> mInstallingPkgsCached;
+ private List<IconRequestInfo<WorkspaceItemInfo>> mWorkspaceIconRequestInfos = new ArrayList<>();
+
private boolean mStopped;
private final Set<PackageUserKey> mPendingPackages = new HashSet<>();
@@ -410,7 +415,7 @@
protected void loadWorkspace(
List<CacheableShortcutInfo> allDeepShortcuts,
String selection,
- LoaderMemoryLogger memoryLogger,
+ @Nullable LoaderMemoryLogger memoryLogger,
@Nullable LauncherRestoreEventLogger restoreEventLogger
) {
Trace.beginSection("LoadWorkspace");
@@ -474,13 +479,12 @@
final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
queryPinnedShortcutsForUnlockedUsers(context, unlockedUsers);
- List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos = new ArrayList<>();
-
+ mWorkspaceIconRequestInfos = new ArrayList<>();
WorkspaceItemProcessor itemProcessor = new WorkspaceItemProcessor(c, memoryLogger,
mUserCache, mUserManagerState, mLauncherApps, mPendingPackages,
mShortcutKeyToPinnedShortcuts, mApp, mBgDataModel,
mWidgetProvidersMap, installingPkgs, isSdCardReady,
- widgetInflater, mPmHelper, iconRequestInfos, unlockedUsers,
+ widgetInflater, mPmHelper, mWorkspaceIconRequestInfos, unlockedUsers,
allDeepShortcuts);
if (mStopped) {
@@ -490,7 +494,7 @@
itemProcessor.processItem();
}
}
- tryLoadWorkspaceIconsInBulk(iconRequestInfos);
+ tryLoadWorkspaceIconsInBulk(mWorkspaceIconRequestInfos);
} finally {
IOUtils.closeSilently(c);
}
@@ -621,7 +625,9 @@
for (IconRequestInfo<WorkspaceItemInfo> iconRequestInfo : iconRequestInfos) {
WorkspaceItemInfo wai = iconRequestInfo.itemInfo;
if (mIconCache.isDefaultIcon(wai.bitmap, wai.user)) {
- iconRequestInfo.loadWorkspaceIcon(mApp.getContext());
+ logASplit("tryLoadWorkspaceIconsInBulk: default icon found for "
+ + wai.getTargetComponent() + ", will attempt to load from iconBlob");
+ iconRequestInfo.loadIconFromDbBlob(mApp.getContext());
}
}
} finally {
@@ -702,7 +708,7 @@
// Clear the list of apps
mBgAllAppsList.clear();
- List<IconRequestInfo<AppInfo>> iconRequestInfos = new ArrayList<>();
+ List<IconRequestInfo<AppInfo>> allAppsItemRequestInfos = new ArrayList<>();
boolean isWorkProfileQuiet = false;
boolean isPrivateProfileQuiet = false;
for (UserHandle user : profiles) {
@@ -742,15 +748,14 @@
}
}
- iconRequestInfos.add(new IconRequestInfo<>(
- appInfo, app, /* useLowResIcon= */ false));
- mBgAllAppsList.add(
- appInfo, app, false);
+ IconRequestInfo<AppInfo> iconRequestInfo = getAppInfoIconRequestInfo(
+ appInfo, app, mWorkspaceIconRequestInfos);
+ allAppsItemRequestInfos.add(iconRequestInfo);
+ mBgAllAppsList.add(appInfo, app, false);
}
allActivityList.addAll(apps);
}
-
if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
// get all active sessions and add them to the all apps list
for (PackageInstaller.SessionInfo info :
@@ -761,7 +766,7 @@
false);
if (promiseAppInfo != null) {
- iconRequestInfos.add(new IconRequestInfo<>(
+ allAppsItemRequestInfos.add(new IconRequestInfo<>(
promiseAppInfo,
/* launcherActivityInfo= */ null,
promiseAppInfo.getMatchingLookupFlag().useLowRes()));
@@ -770,9 +775,22 @@
}
Trace.beginSection("LoadAllAppsIconsInBulk");
+
try {
- mIconCache.getTitlesAndIconsInBulk(iconRequestInfos);
- iconRequestInfos.forEach(iconRequestInfo ->
+ mIconCache.getTitlesAndIconsInBulk(allAppsItemRequestInfos);
+ if (Flags.restoreArchivedAppIconsFromDb()) {
+ for (IconRequestInfo<AppInfo> iconRequestInfo : allAppsItemRequestInfos) {
+ AppInfo appInfo = iconRequestInfo.itemInfo;
+ if (mIconCache.isDefaultIcon(appInfo.bitmap, appInfo.user)) {
+ logASplit("LoadAllAppsIconsInBulk: default icon found for "
+ + appInfo.getTargetComponent()
+ + ", will attempt to load from iconBlob: "
+ + Arrays.toString(iconRequestInfo.iconBlob));
+ iconRequestInfo.loadIconFromDbBlob(mApp.getContext());
+ }
+ }
+ }
+ allAppsItemRequestInfos.forEach(iconRequestInfo ->
mBgAllAppsList.updateSectionName(iconRequestInfo.itemInfo));
} finally {
Trace.endSection();
@@ -795,6 +813,51 @@
return allActivityList;
}
+ @NonNull
+ @VisibleForTesting
+ IconRequestInfo<AppInfo> getAppInfoIconRequestInfo(
+ AppInfo appInfo,
+ LauncherActivityInfo activityInfo,
+ List<IconRequestInfo<WorkspaceItemInfo>> workspaceRequestInfos
+ ) {
+ if (Flags.restoreArchivedAppIconsFromDb()) {
+ Optional<IconRequestInfo<WorkspaceItemInfo>> workspaceIconRequest =
+ workspaceRequestInfos.stream()
+ .filter(request -> appInfo.getTargetComponent().equals(
+ request.itemInfo.getTargetComponent()))
+ .findFirst();
+
+ if (workspaceIconRequest.isPresent() && activityInfo.getApplicationInfo().isArchived) {
+ logASplit("getAppInfoIconRequestInfo:"
+ + " matching archived info found, loading icon blob into icon request."
+ + " Component=" + appInfo.getTargetComponent());
+ IconRequestInfo<AppInfo> iconRequestInfo = new IconRequestInfo<>(
+ appInfo,
+ activityInfo,
+ workspaceIconRequest.get().iconBlob,
+ false /* useLowResIcon= */
+ );
+ if (!iconRequestInfo.loadIconFromDbBlob(mApp.getContext())) {
+ Log.d(TAG, "AppInfo Icon failed to load from blob, using cache.");
+ mIconCache.getTitleAndIcon(
+ appInfo,
+ iconRequestInfo.launcherActivityInfo,
+ DEFAULT_LOOKUP_FLAG
+ );
+ }
+ return iconRequestInfo;
+ } else {
+ Log.d(TAG, "App not archived or workspace info not found"
+ + ", creating IconRequestInfo without icon blob."
+ + " Component:" + appInfo.getTargetComponent()
+ + ", isArchived: " + activityInfo.getApplicationInfo().isArchived);
+ }
+ }
+ logASplit("Loading IconRequestInfo without iconBlob for AppInfo: "
+ + appInfo.getTargetComponent());
+ return new IconRequestInfo<>(appInfo, activityInfo, false /* useLowResIcon= */);
+ }
+
private List<ShortcutInfo> loadDeepShortcuts() {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
mBgDataModel.deepShortcutMap.clear();
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index de1df2e..90f11a3 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -286,7 +286,7 @@
// If the pinned deep shortcut is no longer published,
// use the last saved icon instead of the default.
val csi = CacheableShortcutInfo(pinnedShortcut, appInfoWrapper)
- iconCache.getShortcutIcon(info, csi, c::loadIcon)
+ iconCache.getShortcutIcon(info, csi, c::loadIconFromDb)
if (appInfoWrapper.isSuspended()) {
info.runtimeStatusFlags =
info.runtimeStatusFlags or ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED
diff --git a/src/com/android/launcher3/model/data/IconRequestInfo.java b/src/com/android/launcher3/model/data/IconRequestInfo.java
index e77e527..42af018 100644
--- a/src/com/android/launcher3/model/data/IconRequestInfo.java
+++ b/src/com/android/launcher3/model/data/IconRequestInfo.java
@@ -64,23 +64,25 @@
}
/**
- * Loads this request's item info's title. This method should only be used on IconRequestInfos
- * for WorkspaceItemInfos.
+ * Loads this request's item info's title and icon from given iconBlob from Launcher.db.
+ * This method should only be used on {@link IconRequestInfo} for {@link WorkspaceItemInfo}
+ * or {@link AppInfo}.
*/
- public boolean loadWorkspaceIcon(Context context) {
- if (!(itemInfo instanceof WorkspaceItemInfo)) {
+ public boolean loadIconFromDbBlob(Context context) {
+ if (!(itemInfo instanceof WorkspaceItemInfo) && !(itemInfo instanceof AppInfo)) {
throw new IllegalStateException(
- "loadWorkspaceIcon should only be use for a WorkspaceItemInfos: " + itemInfo);
+ "loadIconFromDb should only be used for either WorkspaceItemInfo or AppInfo: "
+ + itemInfo);
}
try (LauncherIcons li = LauncherIcons.obtain(context)) {
- WorkspaceItemInfo info = (WorkspaceItemInfo) itemInfo;
- // Failed to load from resource, try loading from DB.
+ ItemInfoWithIcon info = itemInfo;
if (iconBlob == null) {
+ Log.d(TAG, "loadIconFromDb: icon blob null, returning. Component="
+ + info.getTargetComponent());
return false;
}
- info.bitmap = li.createIconBitmap(decodeByteArray(
- iconBlob, 0, iconBlob.length));
+ info.bitmap = li.createIconBitmap(decodeByteArray(iconBlob, 0, iconBlob.length));
return true;
} catch (Exception e) {
Log.e(TAG, "Failed to decode byte array for info " + itemInfo, e);
diff --git a/src/com/android/launcher3/shapes/IconShapesProvider.kt b/src/com/android/launcher3/shapes/IconShapesProvider.kt
deleted file mode 100644
index 8608437..0000000
--- a/src/com/android/launcher3/shapes/IconShapesProvider.kt
+++ /dev/null
@@ -1,78 +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.shapes
-
-import com.android.launcher3.Flags as LauncherFlags
-import com.android.systemui.shared.Flags
-
-object IconShapesProvider {
- val shapes =
- if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
- mapOf(
- "arch" to
- IconShapeModel(
- key = "arch",
- title = "arch",
- pathString =
- "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
- ),
- "4_sided_cookie" to
- IconShapeModel(
- key = "4_sided_cookie",
- title = "4 sided cookie",
- pathString =
- "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
- ),
- "seven_sided_cookie" to
- IconShapeModel(
- key = "seven_sided_cookie",
- title = "7 sided cookie",
- pathString =
- "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
- ),
- "sunny" to
- IconShapeModel(
- key = "sunny",
- title = "sunny",
- pathString =
- "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
- ),
- "circle" to
- IconShapeModel(
- key = "circle",
- title = "circle",
- pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
- ),
- "square" to
- IconShapeModel(
- key = "square",
- title = "square",
- pathString =
- "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
- ),
- )
- } else {
- mapOf(
- "circle" to
- IconShapeModel(
- key = "circle",
- title = "circle",
- pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
- )
- )
- }
-}
diff --git a/src/com/android/launcher3/shapes/ShapesProvider.kt b/src/com/android/launcher3/shapes/ShapesProvider.kt
new file mode 100644
index 0000000..dfb7793
--- /dev/null
+++ b/src/com/android/launcher3/shapes/ShapesProvider.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2025 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.shapes
+
+import com.android.launcher3.Flags as LauncherFlags
+import com.android.systemui.shared.Flags
+
+object ShapesProvider {
+ val folderShapes =
+ if (LauncherFlags.enableLauncherIconShapes()) {
+ mapOf(
+ "clover" to
+ "M 39.616 4" +
+ "C 46.224 6.87 53.727 6.87 60.335 4" +
+ "L 63.884 2.459" +
+ "C 85.178 -6.789 106.789 14.822 97.541 36.116" +
+ "L 96 39.665" +
+ "C 93.13 46.273 93.13 53.776 96 60.384" +
+ "L 97.541 63.934" +
+ "C 106.789 85.227 85.178 106.839 63.884 97.591" +
+ "L 60.335 96.049" +
+ "C 53.727 93.179 46.224 93.179 39.616 96.049" +
+ "L 36.066 97.591" +
+ "C 14.773 106.839 -6.839 85.227 2.409 63.934" +
+ "L 3.951 60.384" +
+ "C 6.821 53.776 6.821 46.273 3.951 39.665" +
+ "L 2.409 36.116" +
+ "C -6.839 14.822 14.773 -6.789 36.066 2.459" +
+ "Z",
+ "complexClover" to
+ "M 49.85 6.764" +
+ "L 50.013 6.971" +
+ "L 50.175 6.764" +
+ "C 53.422 2.635 58.309 0.207 63.538 0.207" +
+ "C 65.872 0.207 68.175 0.692 70.381 1.648" +
+ "L 71.79 2.264" +
+ "L 71.792 2.265" +
+ "A 3.46 3.46 0 0 0 74.515 2.265" +
+ "L 74.517 2.264" +
+ "L 75.926 1.652" +
+ "A 17.1 17.1 0 0 1 82.769 0.207" +
+ "C 88.495 0.207 93.824 3.117 97.022 7.989" +
+ "C 100.21 12.848 100.697 18.712 98.36 24.087" +
+ "L 97.749 25.496" +
+ "V 25.497" +
+ "A 3.45 3.45 0 0 0 97.749 28.222" +
+ "V 28.223" +
+ "L 98.36 29.632" +
+ "C 100.697 35.007 100.207 40.871 97.022 45.73" +
+ "A 17.5 17.5 0 0 1 93.264 49.838" +
+ "L 93.06 50" +
+ "L 93.264 50.162" +
+ "A 17.5 17.5 0 0 1 97.022 54.27" +
+ "C 100.21 59.129 100.697 64.993 98.36 70.368" +
+ "V 71.778" +
+ "A 3.45 3.45 0 0 0 97.749 74.503" +
+ "V 74.504" +
+ "L 98.36 75.913" +
+ "C 100.697 81.288 100.207 87.152 97.022 92.011" +
+ "C 93.824 96.883 88.495 99.793 82.769 99.793" +
+ "C 80.435 99.793 78.132 99.308 75.926 98.348" +
+ "L 74.517 97.736" +
+ "H 74.515" +
+ "A 3.5 3.5 0 0 0 73.153 97.455" +
+ "C 72.682 97.455 72.225 97.552 71.792 97.736" +
+ "H 71.79" +
+ "L 70.381 98.348" +
+ "A 17.1 17.1 0 0 1 63.538 99.793" +
+ "C 58.309 99.793 53.422 97.365 50.175 93.236" +
+ "L 50.013 93.029" +
+ "L 49.85 93.236" +
+ "C 46.603 97.365 41.717 99.793 36.488 99.793" +
+ "C 34.154 99.793 31.851 99.308 29.645 98.348" +
+ "L 28.236 97.736" +
+ "H 28.234" +
+ "A 3.5 3.5 0 0 0 26.872 97.455" +
+ "C 26.401 97.455 25.944 97.552 25.511 97.736" +
+ "H 25.509" +
+ "L 24.1 98.348" +
+ "A 17.1 17.1 0 0 1 17.257 99.793" +
+ "C 11.53 99.793 6.202 96.883 3.004 92.011" +
+ "C -0.181 87.152 -0.671 81.288 1.661 75.913" +
+ "L 2.277 74.504" +
+ "V 74.503" +
+ "A 3.45 3.45 0 0 0 2.277 71.778" +
+ "V 71.777" +
+ "L 1.665 70.368" +
+ "C -0.671 64.993 -0.181 59.129 3.004 54.274" +
+ "A 17.5 17.5 0 0 1 6.761 50.162" +
+ "L 6.965 50" +
+ "L 6.761 49.838" +
+ "A 17.5 17.5 0 0 1 3.004 45.73" +
+ "C -0.181 40.871 -0.671 35.007 1.665 29.632" +
+ "L 2.277 28.223" +
+ "V 28.222" +
+ "A 3.45 3.45 0 0 0 2.277 25.497" +
+ "V 25.496" +
+ "L 1.665 24.087" +
+ "C -0.671 18.712 -0.181 12.848 3.004 7.994" +
+ "V 7.993" +
+ "C 6.202 3.117 11.53 0.207 17.257 0.207" +
+ "C 19.591 0.207 21.894 0.692 24.1 1.652" +
+ "L 25.509 2.264" +
+ "L 25.511 2.265" +
+ "A 3.46 3.46 0 0 0 28.234 2.265" +
+ "L 28.236 2.264" +
+ "L 29.645 1.652" +
+ "A 17.1 17.1 0 0 1 36.488 0.207" +
+ "C 41.717 0.207 46.603 2.635 49.85 6.764" +
+ "Z",
+ "arch" to
+ "M 50 0" +
+ "L 72.5 0" +
+ "A 27.5 27.5 0 0 1 100 27.5" +
+ "L 100 86.67" +
+ "A 13.33 13.33 0 0 1 86.67 100" +
+ "L 13.33 100" +
+ "A 13.33 13.33 0 0 1 0 86.67" +
+ "L 0 27.5" +
+ "A 27.5 27.5 0 0 1 27.5 0" +
+ "Z",
+ "square" to
+ "M 50 0" +
+ "L 83.4 0" +
+ "A 16.6 16.6 0 0 1 100 16.6" +
+ "L 100 83.4" +
+ "A 16.6 16.6 0 0 1 83.4 100" +
+ "L 16.6 100" +
+ "A 16.6 16.6 0 0 1 0 83.4" +
+ "L 0 16.6" +
+ "A 16.6 16.6 0 0 1 16.6 0" +
+ "Z",
+ )
+ } else {
+ mapOf("circle" to "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0")
+ }
+
+ val iconShapes =
+ if (Flags.newCustomizationPickerUi() && LauncherFlags.enableLauncherIconShapes()) {
+ mapOf(
+ "arch" to
+ IconShapeModel(
+ key = "arch",
+ title = "arch",
+ pathString =
+ "M50 0C77.614 0 100 22.386 100 50C100 85.471 100 86.476 99.9 87.321 99.116 93.916 93.916 99.116 87.321 99.9 86.476 100 85.471 100 83.46 100H16.54C14.529 100 13.524 100 12.679 99.9 6.084 99.116 .884 93.916 .1 87.321 0 86.476 0 85.471 0 83.46L0 50C0 22.386 22.386 0 50 0Z",
+ folderPathString = folderShapes["arch"]!!,
+ ),
+ "four_sided_cookie" to
+ IconShapeModel(
+ key = "four_sided_cookie",
+ title = "4 sided cookie",
+ pathString =
+ "M39.888,4.517C46.338 7.319 53.662 7.319 60.112 4.517L63.605 3C84.733 -6.176 106.176 15.268 97 36.395L95.483 39.888C92.681 46.338 92.681 53.662 95.483 60.112L97 63.605C106.176 84.732 84.733 106.176 63.605 97L60.112 95.483C53.662 92.681 46.338 92.681 39.888 95.483L36.395 97C15.267 106.176 -6.176 84.732 3 63.605L4.517 60.112C7.319 53.662 7.319 46.338 4.517 39.888L3 36.395C -6.176 15.268 15.267 -6.176 36.395 3Z",
+ folderPathString = folderShapes["complexClover"]!!,
+ ),
+ "seven_sided_cookie" to
+ IconShapeModel(
+ key = "seven_sided_cookie",
+ title = "7 sided cookie",
+ pathString =
+ "M35.209 4.878C36.326 3.895 36.884 3.404 37.397 3.006 44.82 -2.742 55.18 -2.742 62.603 3.006 63.116 3.404 63.674 3.895 64.791 4.878 65.164 5.207 65.351 5.371 65.539 5.529 68.167 7.734 71.303 9.248 74.663 9.932 74.902 9.981 75.147 10.025 75.637 10.113 77.1 10.375 77.831 10.506 78.461 10.66 87.573 12.893 94.032 21.011 94.176 30.412 94.186 31.062 94.151 31.805 94.08 33.293 94.057 33.791 94.045 34.04 94.039 34.285 93.958 37.72 94.732 41.121 96.293 44.18 96.404 44.399 96.522 44.618 96.759 45.056 97.467 46.366 97.821 47.021 98.093 47.611 102.032 56.143 99.727 66.266 92.484 72.24 91.983 72.653 91.381 73.089 90.177 73.961 89.774 74.254 89.572 74.4 89.377 74.548 86.647 76.626 84.477 79.353 83.063 82.483 82.962 82.707 82.865 82.936 82.671 83.395 82.091 84.766 81.8 85.451 81.51 86.033 77.31 94.44 67.977 98.945 58.801 96.994 58.166 96.859 57.451 96.659 56.019 96.259 55.54 96.125 55.3 96.058 55.063 95.998 51.74 95.154 48.26 95.154 44.937 95.998 44.699 96.058 44.46 96.125 43.981 96.259 42.549 96.659 41.834 96.859 41.199 96.994 32.023 98.945 22.69 94.44 18.49 86.033 18.2 85.451 17.909 84.766 17.329 83.395 17.135 82.936 17.038 82.707 16.937 82.483 15.523 79.353 13.353 76.626 10.623 74.548 10.428 74.4 10.226 74.254 9.823 73.961 8.619 73.089 8.017 72.653 7.516 72.24 .273 66.266 -2.032 56.143 1.907 47.611 2.179 47.021 2.533 46.366 3.241 45.056 3.478 44.618 3.596 44.399 3.707 44.18 5.268 41.121 6.042 37.72 5.961 34.285 5.955 34.04 5.943 33.791 5.92 33.293 5.849 31.805 5.814 31.062 5.824 30.412 5.968 21.011 12.427 12.893 21.539 10.66 22.169 10.506 22.9 10.375 24.363 10.113 24.853 10.025 25.098 9.981 25.337 9.932 28.697 9.248 31.833 7.734 34.461 5.529 34.649 5.371 34.836 5.207 35.209 4.878Z",
+ folderPathString = folderShapes["clover"]!!,
+ ),
+ "sunny" to
+ IconShapeModel(
+ key = "sunny",
+ title = "sunny",
+ pathString =
+ "M42.846 4.873C46.084 -.531 53.916 -.531 57.154 4.873L60.796 10.951C62.685 14.103 66.414 15.647 69.978 14.754L76.851 13.032C82.962 11.5 88.5 17.038 86.968 23.149L85.246 30.022C84.353 33.586 85.897 37.315 89.049 39.204L95.127 42.846C100.531 46.084 100.531 53.916 95.127 57.154L89.049 60.796C85.897 62.685 84.353 66.414 85.246 69.978L86.968 76.851C88.5 82.962 82.962 88.5 76.851 86.968L69.978 85.246C66.414 84.353 62.685 85.898 60.796 89.049L57.154 95.127C53.916 100.531 46.084 100.531 42.846 95.127L39.204 89.049C37.315 85.898 33.586 84.353 30.022 85.246L23.149 86.968C17.038 88.5 11.5 82.962 13.032 76.851L14.754 69.978C15.647 66.414 14.103 62.685 10.951 60.796L4.873 57.154C -.531 53.916 -.531 46.084 4.873 42.846L10.951 39.204C14.103 37.315 15.647 33.586 14.754 30.022L13.032 23.149C11.5 17.038 17.038 11.5 23.149 13.032L30.022 14.754C33.586 15.647 37.315 14.103 39.204 10.951L42.846 4.873Z",
+ folderPathString = folderShapes["clover"]!!,
+ ),
+ "circle" to
+ IconShapeModel(
+ key = "circle",
+ title = "circle",
+ pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+ folderPathString = folderShapes["clover"]!!,
+ ),
+ "square" to
+ IconShapeModel(
+ key = "square",
+ title = "square",
+ pathString =
+ "M53.689 0.82 L53.689 .82 C67.434 .82 74.306 .82 79.758 2.978 87.649 6.103 93.897 12.351 97.022 20.242 99.18 25.694 99.18 32.566 99.18 46.311 V53.689 C99.18 67.434 99.18 74.306 97.022 79.758 93.897 87.649 87.649 93.897 79.758 97.022 74.306 99.18 67.434 99.18 53.689 99.18 H46.311 C32.566 99.18 25.694 99.18 20.242 97.022 12.351 93.897 6.103 87.649 2.978 79.758 .82 74.306 .82 67.434 .82 53.689 L.82 46.311 C.82 32.566 .82 25.694 2.978 20.242 6.103 12.351 12.351 6.103 20.242 2.978 25.694 .82 32.566 .82 46.311 .82Z",
+ folderShapes["square"]!!,
+ ),
+ )
+ } else {
+ mapOf(
+ "default" to
+ IconShapeModel(
+ key = "default",
+ title = "circle",
+ pathString = "M50 0A50 50,0,1,1,50 100A50 50,0,1,1,50 0",
+ )
+ )
+ }
+}
diff --git a/src/com/android/launcher3/touch/WorkspaceTouchListener.java b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
index b69bc17..d72e6f9 100644
--- a/src/com/android/launcher3/touch/WorkspaceTouchListener.java
+++ b/src/com/android/launcher3/touch/WorkspaceTouchListener.java
@@ -21,6 +21,7 @@
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
+import static com.android.launcher3.Flags.enableMouseInteractionChanges;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_CLOSE_TAP_OUTSIDE;
@@ -31,6 +32,7 @@
import android.graphics.Rect;
import android.view.GestureDetector;
import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
@@ -41,7 +43,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.Workspace;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.logger.LauncherAtom;
import com.android.launcher3.testing.TestLogging;
@@ -193,6 +194,10 @@
@Override
public void onLongPress(MotionEvent event) {
+ if (event.getSource() == InputDevice.SOURCE_MOUSE && enableMouseInteractionChanges()) {
+ // Stop mouse long press events from showing the menu.
+ return;
+ }
maybeShowMenu();
}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 4811a17..7a27bf4 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -153,17 +153,19 @@
mWidgetAddButton = findViewById(R.id.widget_add_button);
if (enableWidgetTapToAdd()) {
-
setAccessibilityDelegate(new AccessibilityDelegate() {
@Override
public void onInitializeAccessibilityNodeInfo(View host,
AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
- String accessibilityLabel = getResources().getString(mWidgetAddButton.isShown()
- ? R.string.widget_cell_tap_to_hide_add_button_label
- : R.string.widget_cell_tap_to_show_add_button_label);
- info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
- accessibilityLabel));
+ if (hasOnClickListeners()) {
+ String accessibilityLabel = getResources().getString(
+ mWidgetAddButton.isShown()
+ ? R.string.widget_cell_tap_to_hide_add_button_label
+ : R.string.widget_cell_tap_to_show_add_button_label);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK,
+ accessibilityLabel));
+ }
}
});
mWidgetAddButton.setVisibility(INVISIBLE);
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
index 63359ec..11047fb 100644
--- a/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -16,6 +16,9 @@
package com.android.launcher3.model;
+import static android.graphics.BitmapFactory.decodeByteArray;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
import static androidx.test.InstrumentationRegistry.getContext;
import static com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID;
@@ -40,8 +43,11 @@
import static com.android.launcher3.LauncherSettings.Favorites.SPANY;
import static com.android.launcher3.LauncherSettings.Favorites.TITLE;
import static com.android.launcher3.LauncherSettings.Favorites._ID;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
@@ -52,13 +58,19 @@
import android.content.Context;
import android.content.Intent;
import android.database.MatrixCursor;
+import android.graphics.Bitmap;
import android.os.Process;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.Executors;
@@ -67,6 +79,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -77,6 +90,9 @@
@RunWith(AndroidJUnit4.class)
public class LoaderCursorTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
private LauncherModelHelper mModelHelper;
private LauncherAppState mApp;
private PackageManagerHelper mPmHelper;
@@ -87,6 +103,12 @@
private LoaderCursor mLoaderCursor;
+ private static byte[] sTestBlob = new byte[] {
+ -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1,
+ 8, 4, 0, 0, 0, -75, 28, 12, 2, 0, 0, 0, 11, 73, 68, 65, 84, 120, -38, 99, 100, 96, 0,
+ 0, 0, 6, 0, 2, 48, -127, -48, 47, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126
+ };
+
@Before
public void setup() {
mModelHelper = new LauncherModelHelper();
@@ -119,7 +141,8 @@
.add(PROFILE_ID, 0)
.add(ITEM_TYPE, itemType)
.add(TITLE, title)
- .add(CONTAINER, CONTAINER_DESKTOP);
+ .add(CONTAINER, CONTAINER_DESKTOP)
+ .add(ICON, sTestBlob);
}
@Test
@@ -161,7 +184,12 @@
@Test
public void loadSimpleShortcut() {
- initCursor(ITEM_TYPE_DEEP_SHORTCUT, "my-shortcut");
+ mCursor.newRow()
+ .add(_ID, 1)
+ .add(PROFILE_ID, 0)
+ .add(ITEM_TYPE, ITEM_TYPE_DEEP_SHORTCUT)
+ .add(TITLE, "my-shortcut")
+ .add(CONTAINER, CONTAINER_DESKTOP);
assertTrue(mLoaderCursor.moveToNext());
WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
@@ -223,6 +251,68 @@
newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3), true));
}
+ @Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ public void ifArchivedWithFlag_whenloadWorkspaceTitleAndIcon_thenLoadIconFromDb() {
+ // Given
+ initCursor(ITEM_TYPE_APPLICATION, "title");
+ assertTrue(mLoaderCursor.moveToNext());
+ WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+ itemInfo.bitmap = null;
+ itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+ Bitmap expectedBitmap = LauncherIcons.obtain(mContext)
+ .createIconBitmap(decodeByteArray(sTestBlob, 0, sTestBlob.length))
+ .icon;
+ // When
+ mLoaderCursor.loadWorkspaceTitleAndIcon(false, true, itemInfo);
+ // Then
+ assertThat(itemInfo.bitmap.icon).isNotNull();
+ assertThat(itemInfo.bitmap.icon.sameAs(expectedBitmap)).isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ public void ifArchivedWithFlag_whenLoadIconFromDb_thenLoadIconFromBlob() {
+ // Given
+ initCursor(ITEM_TYPE_APPLICATION, "title");
+ assertTrue(mLoaderCursor.moveToNext());
+ WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+ itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+ // Then
+ assertTrue(mLoaderCursor.loadIconFromDb(itemInfo));
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ public void ifArchivedWithoutFlag_whenLoadWorkspaceTitleAndIcon_thenDoNotLoadFromDb() {
+ // Given
+ initCursor(ITEM_TYPE_APPLICATION, "title");
+ assertTrue(mLoaderCursor.moveToNext());
+ WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+ itemInfo.bitmap = null;
+ itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("package", "class"));
+ itemInfo.intent = intent;
+ // When
+ mLoaderCursor.loadWorkspaceTitleAndIcon(false, false, itemInfo);
+ // Then
+ assertThat(itemInfo.bitmap).isNull();
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ public void ifArchivedWithoutFlag_whenLoadIconFromDb_thenDoNotLoadFromBlob() {
+ // Given
+ initCursor(ITEM_TYPE_APPLICATION, "title");
+ assertTrue(mLoaderCursor.moveToNext());
+ WorkspaceItemInfo itemInfo = new WorkspaceItemInfo();
+ itemInfo.runtimeStatusFlags |= FLAG_ARCHIVED;
+ // Then
+ assertFalse(mLoaderCursor.loadIconFromDb(itemInfo));
+ }
+
+
private ItemInfo newItemInfo(int cellX, int cellY, int spanX, int spanY,
int container, int screenId) {
ItemInfo info = new ItemInfo();
diff --git a/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
similarity index 63%
rename from tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
index 234e050..2b8896e 100644
--- a/tests/multivalentTests/src/com/android/launcher3/shapes/IconShapesProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/shapes/ShapesProviderTest.kt
@@ -30,14 +30,14 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-class IconShapesProviderTest {
+class ShapesProviderTest {
@get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path arch`() {
- IconShapesProvider.shapes["arch"]?.apply {
+ ShapesProvider.iconShapes["arch"]?.apply {
GenericPathShape(pathString)
PathParser.createPathFromPathData(pathString)
}
@@ -46,7 +46,7 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path 4_sided_cookie`() {
- IconShapesProvider.shapes["4_sided_cookie"]?.apply {
+ ShapesProvider.iconShapes["4_sided_cookie"]?.apply {
GenericPathShape(pathString)
PathParser.createPathFromPathData(pathString)
}
@@ -55,7 +55,7 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path seven_sided_cookie`() {
- IconShapesProvider.shapes["seven_sided_cookie"]?.apply {
+ ShapesProvider.iconShapes["seven_sided_cookie"]?.apply {
GenericPathShape(pathString)
PathParser.createPathFromPathData(pathString)
}
@@ -64,7 +64,7 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path sunny`() {
- IconShapesProvider.shapes["sunny"]?.apply {
+ ShapesProvider.iconShapes["sunny"]?.apply {
GenericPathShape(pathString)
PathParser.createPathFromPathData(pathString)
}
@@ -73,7 +73,7 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path circle`() {
- IconShapesProvider.shapes["circle"]?.apply {
+ ShapesProvider.iconShapes["circle"]?.apply {
GenericPathShape(pathString)
PathParser.createPathFromPathData(pathString)
}
@@ -82,7 +82,43 @@
@Test
@EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
fun `verify valid path square`() {
- IconShapesProvider.shapes["square"]?.apply {
+ ShapesProvider.iconShapes["square"]?.apply {
+ GenericPathShape(pathString)
+ PathParser.createPathFromPathData(pathString)
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+ fun `verify valid folder path clover`() {
+ ShapesProvider.folderShapes["clover"]?.let { pathString ->
+ GenericPathShape(pathString)
+ PathParser.createPathFromPathData(pathString)
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+ fun `verify valid folder path complexClover`() {
+ ShapesProvider.folderShapes["complexClover"]?.let { pathString ->
+ GenericPathShape(pathString)
+ PathParser.createPathFromPathData(pathString)
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+ fun `verify valid folder path arch`() {
+ ShapesProvider.folderShapes["arch"]?.let { pathString ->
+ GenericPathShape(pathString)
+ PathParser.createPathFromPathData(pathString)
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_LAUNCHER_ICON_SHAPES, FLAG_NEW_CUSTOMIZATION_PICKER_UI)
+ fun `verify valid folder path square`() {
+ ShapesProvider.folderShapes["square"]?.let { pathString ->
GenericPathShape(pathString)
PathParser.createPathFromPathData(pathString)
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index faf6b91..5c326f9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/multivalentTests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -27,7 +27,6 @@
import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING;
import static com.android.launcher3.Flags.FLAG_USE_NEW_ICON_FOR_ARCHIVED_APPS;
import static com.android.launcher3.LauncherPrefs.ENABLE_TWOLINE_ALLAPPS_TOGGLE;
-import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -81,8 +80,6 @@
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
-import java.util.concurrent.CountDownLatch;
-
/**
* Unit tests for testing modifyTitleToSupportMultiLine() in BubbleTextView.java
* This class tests a couple of strings and uses the getMaxLines() to determine if the test passes.
@@ -518,14 +515,12 @@
// Icon is replaced with a non pending icon when download finishes
mItemInfoWithIcon.setProgressLevel(100, PackageInstallInfo.STATUS_INSTALLED);
- CountDownLatch animWait = new CountDownLatch(1);
TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () -> {
mBubbleTextView.applyIconAndLabel(mItemInfoWithIcon);
assertThat(mBubbleTextView.getIcon()).isSameInstanceAs(oldIcon);
assertThat(oldIcon.getActiveAnimation()).isNotNull();
- oldIcon.getActiveAnimation().addListener(forEndCallback(animWait::countDown));
+ oldIcon.getActiveAnimation().end();
});
- animWait.await();
// Assert that the icon is replaced with a non-pending icon
assertThat(mBubbleTextView.getIcon()).isNotInstanceOf(PreloadIconDrawable.class);
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index dba7603..cdb45fc 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -1,7 +1,10 @@
package com.android.launcher3.model
import android.appwidget.AppWidgetManager
+import android.content.ComponentName
import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
import android.os.Process
import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
@@ -28,6 +31,9 @@
import com.android.launcher3.icons.IconCache
import com.android.launcher3.icons.cache.CachingLogic
import com.android.launcher3.icons.cache.IconCacheUpdateHandler
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.IconRequestInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.RestoreDbTask
import com.android.launcher3.ui.TestViewHelpers
@@ -38,8 +44,11 @@
import com.android.launcher3.util.TestUtil
import com.android.launcher3.util.UserIconInfo
import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
+import java.util.concurrent.CountDownLatch
+import java.util.function.Predicate
import junit.framework.Assert.assertEquals
import org.junit.After
import org.junit.Before
@@ -58,12 +67,11 @@
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
-import java.util.concurrent.CountDownLatch
-import java.util.function.Predicate
private const val INSERTION_STATEMENT_FILE = "databases/workspace_items.sql"
@@ -480,6 +488,157 @@
verify(spyContext, times(0)).sendBroadcast(any())
}
+ @Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ fun `When flag on then archived AllApps icons found on Workspace loaded from db`() {
+ // Given
+ // Given
+ val activityInfo: LauncherActivityInfo = mock()
+ val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = true }
+ whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+ val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+ val expectedComponent = ComponentName("package", "class")
+ val workspaceIconRequests =
+ listOf(
+ IconRequestInfo<WorkspaceItemInfo>(
+ WorkspaceItemInfo().apply {
+ intent = Intent().apply { component = expectedComponent }
+ },
+ activityInfo,
+ expectedIconBlob,
+ false, /* useLowResIcon */
+ )
+ )
+ val expectedAppInfo = AppInfo().apply { componentName = expectedComponent }
+ // When
+ val loader =
+ LoaderTask(
+ app,
+ bgAllAppsList,
+ BgDataModel(),
+ modelDelegate,
+ launcherBinder,
+ widgetsFilterDataProvider,
+ )
+ val actualIconRequest =
+ loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+ // Then
+ assertThat(actualIconRequest.iconBlob).isEqualTo(expectedIconBlob)
+ assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ fun `When flag on then unarchived AllApps icons not loaded from db`() {
+ // Given
+ val activityInfo: LauncherActivityInfo = mock()
+ val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = false }
+ whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+ val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+ val expectedComponent = ComponentName("package", "class")
+ val workspaceIconRequests =
+ listOf(
+ IconRequestInfo<WorkspaceItemInfo>(
+ WorkspaceItemInfo().apply {
+ intent = Intent().apply { component = expectedComponent }
+ },
+ activityInfo,
+ expectedIconBlob,
+ false, /* useLowResIcon */
+ )
+ )
+ val expectedAppInfo = AppInfo().apply { componentName = expectedComponent }
+ // When
+ val loader =
+ LoaderTask(
+ app,
+ bgAllAppsList,
+ BgDataModel(),
+ modelDelegate,
+ launcherBinder,
+ widgetsFilterDataProvider,
+ )
+ val actualIconRequest =
+ loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+ // Then
+ assertThat(actualIconRequest.iconBlob).isNull()
+ assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ fun `When flag on then archived AllApps icon not found on Workspace not loaded from db`() {
+ // Given
+ val activityInfo: LauncherActivityInfo = mock()
+ val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = true }
+ whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+ val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+ val expectedComponent = ComponentName("package", "class")
+ val workspaceIconRequests =
+ listOf(
+ IconRequestInfo<WorkspaceItemInfo>(
+ WorkspaceItemInfo().apply {
+ intent = Intent().apply { component = expectedComponent }
+ },
+ activityInfo,
+ expectedIconBlob,
+ false, /* useLowResIcon */
+ )
+ )
+ val expectedAppInfo =
+ AppInfo().apply { componentName = ComponentName("differentPkg", "differentClass") }
+ // When
+ val loader =
+ LoaderTask(
+ app,
+ bgAllAppsList,
+ BgDataModel(),
+ modelDelegate,
+ launcherBinder,
+ widgetsFilterDataProvider,
+ )
+ val actualIconRequest =
+ loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+ // Then
+ assertThat(actualIconRequest.iconBlob).isNull()
+ assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_RESTORE_ARCHIVED_APP_ICONS_FROM_DB)
+ fun `When flag off then archived AllApps icons not loaded from db`() {
+ // Given
+ val activityInfo: LauncherActivityInfo = mock()
+ val applicationInfo: ApplicationInfo = mock<ApplicationInfo>().apply { isArchived = true }
+ whenever(activityInfo.applicationInfo).thenReturn(applicationInfo)
+ val expectedIconBlob = byteArrayOf(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)
+ val workspaceIconRequests =
+ listOf(
+ IconRequestInfo<WorkspaceItemInfo>(
+ WorkspaceItemInfo(),
+ activityInfo,
+ expectedIconBlob,
+ false, /* useLowResIcon */
+ )
+ )
+ val expectedAppInfo = AppInfo()
+ // When
+ val loader =
+ LoaderTask(
+ app,
+ bgAllAppsList,
+ BgDataModel(),
+ modelDelegate,
+ launcherBinder,
+ widgetsFilterDataProvider,
+ )
+ val actualIconRequest =
+ loader.getAppInfoIconRequestInfo(expectedAppInfo, activityInfo, workspaceIconRequests)
+ // Then
+ assertThat(actualIconRequest.iconBlob).isNull()
+ assertThat(actualIconRequest.itemInfo).isEqualTo(expectedAppInfo)
+ }
+
@LauncherAppSingleton
@Component(modules = [AllModulesForTest::class])
interface TestComponent : LauncherAppComponent {