Merge "Add DeviceProfile dump files into artifacts" into main
diff --git a/aconfig/launcher_overview.aconfig b/aconfig/launcher_overview.aconfig
index f379e22..5749c51 100644
--- a/aconfig/launcher_overview.aconfig
+++ b/aconfig/launcher_overview.aconfig
@@ -96,3 +96,17 @@
description: "Enable overview on connected displays."
bug: "363251602"
}
+
+flag {
+ name: "enable_overview_background_wallpaper_blur"
+ namespace: "launcher_overview"
+ description: "Enable wallpaper blur in overview."
+ bug: "369975912"
+}
+
+flag {
+ name: "enable_overview_desktop_tile_wallpaper_background"
+ namespace: "launcher_overview"
+ description: "Enable wallpaper background for desktop tasks in overview."
+ bug: "363257721"
+}
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
index 3773d02..37e8d4d 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
+++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.os.Debug
import android.util.Log
+import android.util.Slog
import android.util.SparseArray
import android.view.Display.DEFAULT_DISPLAY
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
@@ -152,7 +153,8 @@
return areDesktopTasksVisible()
}
- val isInDesktopMode = displaysDesksConfigsMap[displayId].activeDeskId != INACTIVE_DESK_ID
+ val activeDeskId = getDisplayDeskConfig(displayId)?.activeDeskId ?: INACTIVE_DESK_ID
+ val isInDesktopMode = activeDeskId != INACTIVE_DESK_ID
if (DEBUG) {
Log.d(TAG, "isInDesktopMode: $isInDesktopMode")
}
@@ -409,18 +411,16 @@
}
}
- private fun getDisplayDeskConfig(displayId: Int): DisplayDeskConfig {
- return checkNotNull(displaysDesksConfigsMap[displayId]) {
- "Expected non-null desk config for display: $displayId"
- }
- }
+ private fun getDisplayDeskConfig(displayId: Int) =
+ displaysDesksConfigsMap[displayId]
+ ?: null.also { Slog.e(TAG, "Expected non-null desk config for display: $displayId") }
private fun onCanCreateDesksChanged(displayId: Int, canCreateDesks: Boolean) {
if (!DesktopModeStatus.enableMultipleDesktops(context)) {
return
}
- getDisplayDeskConfig(displayId).canCreateDesks = canCreateDesks
+ getDisplayDeskConfig(displayId)?.canCreateDesks = canCreateDesks
}
private fun onDeskAdded(displayId: Int, deskId: Int) {
@@ -428,7 +428,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.add(deskId)) {
"Found a duplicate desk Id: $deskId on display: $displayId"
}
@@ -440,7 +440,7 @@
return
}
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(it.deskIds.remove(deskId)) {
"Removing non-existing desk Id: $deskId on display: $displayId"
}
@@ -457,7 +457,7 @@
val wasInDesktopMode = isInDesktopModeAndNotInOverview(displayId)
- getDisplayDeskConfig(displayId).also {
+ getDisplayDeskConfig(displayId)?.also {
check(oldActiveDesk == it.activeDeskId) {
"Mismatch between the Shell's oldActiveDesk: $oldActiveDesk, and Launcher's: ${it.activeDeskId}"
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 46802c3..6cd2979 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -747,7 +747,11 @@
/**
* Signal from SysUI indicating that system decorations should be removed from the display.
*/
- public void onDisplayRemoveSystemDecorations(int displayId) {}
+ public void onDisplayRemoveSystemDecorations(int displayId) {
+ // The display mirroring starts. The handling logic is the same as when removing a
+ // display.
+ onDisplayRemoved(displayId);
+ }
private void removeActivityCallbacksAndListeners() {
if (mActivity != null) {
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index be0a339..783ec2c 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -514,7 +514,13 @@
private void finishAnimation() {
mLauncher.setPredictiveBackToHomeInProgress(false);
+ if (mBackTarget != null && mBackTarget.leash.isValid()) {
+ mBackTarget.leash.release();
+ }
mBackTarget = null;
+ if (mLauncherTarget != null && mLauncherTarget.leash.isValid()) {
+ mLauncherTarget.leash.release();
+ }
mLauncherTarget = null;
mBackInProgress = false;
mBackProgress = 0;
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index 42aa86e..afdb403 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -293,10 +293,6 @@
val recentsView: RecentsView<*, *>? = recentsViewContainer?.getOverviewPanel()
val deviceProfile = recentsViewContainer?.getDeviceProfile()
val uiController = containerInterface.getTaskbarController()
- val allowQuickSwitch =
- uiController != null &&
- deviceProfile != null &&
- (deviceProfile.isTablet || deviceProfile.isTwoPanels)
val focusedDisplayId = focusState.focusedDisplayId
val focusedDisplayUIController: TaskbarUIController? =
@@ -322,26 +318,26 @@
when (command.type) {
HIDE -> {
- if (!allowQuickSwitch) return true
+ if (uiController == null || deviceProfile?.isTablet == false) return true
keyboardTaskFocusIndex =
if (
enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
) {
focusedDisplayUIController.launchFocusedTask()
} else {
- uiController!!.launchFocusedTask()
+ uiController.launchFocusedTask()
}
if (keyboardTaskFocusIndex == -1) return true
}
KEYBOARD_INPUT ->
- if (allowQuickSwitch) {
+ if (uiController != null && deviceProfile?.isTablet == true) {
if (
enableAltTabKqsOnConnectedDisplays() && focusedDisplayUIController != null
) {
focusedDisplayUIController.openQuickSwitchView()
} else {
- uiController!!.openQuickSwitchView()
+ uiController.openQuickSwitchView()
}
return true
} else {
@@ -365,7 +361,11 @@
TOGGLE -> {}
}
- recentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex)
+ recentsView?.setKeyboardTaskFocusIndex(
+ recentsView.indexOfChild(recentsView.taskViews.elementAtOrNull(keyboardTaskFocusIndex))
+ ?: -1
+ )
+
// Handle recents view focus when launching from home
val animatorListener: Animator.AnimatorListener =
object : AnimatorListenerAdapter() {
@@ -526,7 +526,7 @@
// Stops requesting focused after first view gets focused.
recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
recentsView.nextTaskView.requestFocus() ||
- recentsView.getFirstTaskView().requestFocus() ||
+ recentsView.firstTaskView.requestFocus() ||
recentsView.requestFocus()
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8100f25..a29d302 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2013,7 +2013,7 @@
}
// If the list changed, maybe the focused task doesn't exist anymore.
if (newFocusedTaskView == null) {
- newFocusedTaskView = mUtils.getExpectedFocusedTask();
+ newFocusedTaskView = mUtils.getFirstNonDesktopTaskView();
}
}
setFocusedTaskViewId(
@@ -2119,15 +2119,6 @@
return mTaskViewCount;
}
- /**
- * Transverse RecentsView children to calculate the amount of DesktopTaskViews.
- *
- * @return Number of children that are instances of DesktopTaskView
- */
- private int getDesktopTaskViewCount() {
- return mUtils.getDesktopTaskViewCount();
- }
-
/** Counts {@link TaskView}s that are not {@link DesktopTaskView} instances. */
public int getNonDesktopTaskViewCount() {
return mUtils.getNonDesktopTaskViewCount();
@@ -3079,7 +3070,7 @@
focusedTaskViewId = INVALID_TASK_ID;
} else if (enableLargeDesktopWindowingTile()
&& getRunningTaskView() instanceof DesktopTaskView) {
- TaskView focusedTaskView = getTaskViewAt(getDesktopTaskViewCount());
+ TaskView focusedTaskView = mUtils.getFirstNonDesktopTaskView();
focusedTaskViewId =
focusedTaskView != null ? focusedTaskView.getTaskViewId() : INVALID_TASK_ID;
} else {
@@ -3950,9 +3941,9 @@
int distanceFromDismissedTask = 1;
int slidingTranslation = 0;
if (isSlidingTasks) {
- int nextSnappedPage = isStagingFocusedTask
- ? indexOfChild(mUtils.getFirstSmallTaskView())
- : mUtils.getDesktopTaskViewCount();
+ int nextSnappedPage = indexOfChild(isStagingFocusedTask
+ ? mUtils.getFirstSmallTaskView()
+ : mUtils.getFirstNonDesktopTaskView());
slidingTranslation = getPagedOrientationHandler().getPrimaryScroll(this)
- getScrollForPage(nextSnappedPage);
slidingTranslation += mIsRtl ? newClearAllShortTotalWidthTranslation
@@ -3978,7 +3969,7 @@
Math.abs(i - dismissedIndex),
scrollDiff,
anim,
- splitTimings, i);
+ splitTimings);
needsCurveUpdates = true;
}
} else if (child instanceof TaskView taskView) {
@@ -4354,8 +4345,7 @@
int indexDiff,
int scrollDiffPerPage,
PendingAnimation pendingAnimation,
- SplitAnimationTimings splitTimings,
- int index) {
+ SplitAnimationTimings splitTimings) {
// No need to translate the AddDesktopButton on dismissing a TaskView, which should be
// always at the right most position, even when dismissing the last TaskView.
if (view instanceof AddDesktopButton) {
@@ -6277,12 +6267,12 @@
}
@Override
- protected int getChildVisibleSize(int index) {
- final TaskView taskView = getTaskViewAt(index);
+ protected int getChildVisibleSize(int childIndex) {
+ final TaskView taskView = getTaskViewAt(childIndex);
if (taskView == null) {
- return super.getChildVisibleSize(index);
+ return super.getChildVisibleSize(childIndex);
}
- return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment(
+ return (int) (super.getChildVisibleSize(childIndex) * taskView.getSizeAdjustment(
showAsFullscreen()));
}
@@ -6341,7 +6331,7 @@
* Returns how many pixels the page is offset on the currently laid out dominant axis.
*/
private int getUnclampedScrollOffset(int pageIndex) {
- if (pageIndex == -1) {
+ if (pageIndex == INVALID_PAGE) {
return 0;
}
// Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 94e8c03..f742ec3 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -73,7 +73,7 @@
}
/** Counts [TaskView]s that are [DesktopTaskView] instances. */
- fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
+ private fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
/** Counts [TaskView]s that are not [DesktopTaskView] instances. */
fun getNonDesktopTaskViewCount(): Int = taskViews.count { it !is DesktopTaskView }
@@ -91,7 +91,7 @@
}
/** Returns the expected focus task. */
- fun getExpectedFocusedTask(): TaskView? =
+ fun getFirstNonDesktopTaskView(): TaskView? =
if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
else taskViews.firstOrNull()
diff --git a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
index b9d2fed..6f0aaeb 100644
--- a/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
+++ b/quickstep/tests/multivalentScreenshotTests/src/com/android/quickstep/task/thumbnail/TaskThumbnailViewScreenshotTest.kt
@@ -16,10 +16,19 @@
package com.android.quickstep.task.thumbnail
import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.drawable.BitmapDrawable
import android.view.LayoutInflater
+import android.view.Surface.ROTATION_0
+import androidx.core.graphics.set
import com.android.launcher3.R
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot
+import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.google.android.apps.nexuslauncher.imagecomparison.goldenpathmanager.ViewScreenshotGoldenPathManager
import org.junit.Rule
@@ -81,6 +90,96 @@
}
@Test
+ fun taskThumbnailView_liveTile_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_liveTile") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(TaskThumbnailUiState.LiveTile.WithoutHeader)
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_image_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_image") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ null,
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_image_withoutHeader_withImageMatrix() {
+ screenshotRule.screenshotTest("taskThumbnailView_image_withMatrix") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(
+ createBitmap(
+ width = VIEW_ENV_WIDTH / 2,
+ height = lessThanHeightMatchingAspectRatio,
+ ),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
+ null,
+ )
+ )
+ setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_splash_withoutHeader() {
+ screenshotRule.screenshotTest("taskThumbnailView_partial_splash") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(createBitmap(), ROTATION_0, Color.DKGRAY),
+ BitmapDrawable(activity.resources, createSplash()),
+ )
+ )
+ updateSplashAlpha(0.5f)
+ }
+ }
+ }
+
+ @Test
+ fun taskThumbnailView_splash_withoutHeader_withImageMatrix() {
+ screenshotRule.screenshotTest("taskThumbnailView_partial_splash_withMatrix") { activity ->
+ activity.actionBar?.hide()
+ createTaskThumbnailView(activity).apply {
+ val lessThanHeightMatchingAspectRatio = (VIEW_ENV_HEIGHT / 2) - 200
+ setState(
+ SnapshotSplash(
+ Snapshot.WithoutHeader(
+ createBitmap(
+ width = VIEW_ENV_WIDTH / 2,
+ height = lessThanHeightMatchingAspectRatio,
+ ),
+ ROTATION_0,
+ Color.DKGRAY,
+ ),
+ BitmapDrawable(activity.resources, createSplash()),
+ )
+ )
+ setImageMatrix(Matrix().apply { postScale(2f, 2f) })
+ updateSplashAlpha(0.5f)
+ }
+ }
+ }
+
+ @Test
fun taskThumbnailView_dimmed_tintAmount() {
screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity ->
activity.actionBar?.hide()
@@ -122,6 +221,27 @@
return taskThumbnailView
}
+ private fun createSplash() = createBitmap(width = 20, height = 20, rectColorRotation = 1)
+
+ private fun createBitmap(
+ width: Int = VIEW_ENV_WIDTH,
+ height: Int = VIEW_ENV_HEIGHT,
+ rectColorRotation: Int = 0,
+ ) =
+ Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
+ Canvas(this).apply {
+ val paint = Paint()
+ paint.color = BITMAP_RECT_COLORS[rectColorRotation % 4]
+ drawRect(0f, 0f, width / 2f, height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(1 + rectColorRotation) % 4]
+ drawRect(width / 2f, 0f, width.toFloat(), height / 2f, paint)
+ paint.color = BITMAP_RECT_COLORS[(2 + rectColorRotation) % 4]
+ drawRect(0f, height / 2f, width / 2f, height.toFloat(), paint)
+ paint.color = BITMAP_RECT_COLORS[(3 + rectColorRotation) % 4]
+ drawRect(width / 2f, height / 2f, width.toFloat(), height.toFloat(), paint)
+ }
+ }
+
companion object {
@Parameters(name = "{0}")
@JvmStatic
@@ -133,5 +253,8 @@
)
const val CORNER_RADIUS = 56f
+ val BITMAP_RECT_COLORS = listOf(Color.GREEN, Color.RED, Color.BLUE, Color.CYAN)
+ const val VIEW_ENV_WIDTH = 1440
+ const val VIEW_ENV_HEIGHT = 3120
}
}
diff --git a/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
new file mode 100644
index 0000000..4b8f2a2
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/statehandlers/DesktopVisibilityControllerTest.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.statehandlers
+
+import android.content.Context
+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.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.launcher3.util.DaggerSingletonTracker
+import com.android.quickstep.SystemUiProxy
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
+import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import org.junit.After
+import org.junit.Assert.assertFalse
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.mockito.quality.Strictness
+
+/**
+ * Tests the behavior of [DesktopVisibilityController] in regards to multiple desktops and multiple
+ * displays.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DesktopVisibilityControllerTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus::class.java)
+ .startMocking()
+
+ private val context = mock<Context>()
+ private val systemUiProxy = mock<SystemUiProxy>()
+ private val lifeCycleTracker = mock<DaggerSingletonTracker>()
+ private lateinit var desktopVisibilityController: DesktopVisibilityController
+
+ @Before
+ fun setUp() {
+ whenever(context.resources).thenReturn(mock())
+ whenever(DesktopModeStatus.enableMultipleDesktops(context)).thenReturn(true)
+ desktopVisibilityController =
+ DesktopVisibilityController(context, systemUiProxy, lifeCycleTracker)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_MULTIPLE_DESKTOPS_FRONTEND)
+ fun noCrashWhenCheckingNonExistentDisplay() {
+ assertFalse(desktopVisibilityController.isInDesktopMode(displayId = 500))
+ assertFalse(desktopVisibilityController.isInDesktopModeAndNotInOverview(displayId = 300))
+ }
+}
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index c2d6df5..870e6d6 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -85,6 +85,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import javax.inject.Inject;
@@ -252,7 +253,7 @@
public Point defaultWallpaperSize;
- private final ArrayList<OnIDPChangeListener> mChangeListeners = new ArrayList<>();
+ private final List<OnIDPChangeListener> mChangeListeners = new CopyOnWriteArrayList<>();
@Inject
InvariantDeviceProfile(
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 71013c3..bf2ad92 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,54 +16,32 @@
package com.android.launcher3;
-import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
-import static android.content.Context.RECEIVER_EXPORTED;
-
-import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
-import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
-import static com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI;
-import static com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
-
-import android.content.ComponentName;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.ArchiveCompatibilityParams;
import android.util.Log;
import androidx.annotation.Nullable;
-import androidx.core.os.BuildCompat;
+import com.android.launcher3.dagger.LauncherComponentProvider;
import com.android.launcher3.graphics.ThemeManager;
-import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.icons.LauncherIconProvider;
-import com.android.launcher3.icons.LauncherIcons;
-import com.android.launcher3.model.ModelLauncherCallbacks;
+import com.android.launcher3.model.ModelInitializer;
import com.android.launcher3.model.WidgetsFilterDataProvider;
-import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.pm.InstallSessionHelper;
-import com.android.launcher3.pm.InstallSessionTracker;
import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.SimpleBroadcastReceiver;
import com.android.launcher3.util.TraceHelper;
import com.android.launcher3.widget.custom.CustomWidgetManager;
public class LauncherAppState implements SafeCloseable {
public static final String TAG = "LauncherAppState";
- public static final String ACTION_FORCE_ROLOAD = "force-reload-launcher";
// We do not need any synchronization for this variable as its only written on UI thread.
public static final MainThreadInitializedObject<LauncherAppState> INSTANCE =
@@ -94,82 +72,21 @@
mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> context.getPackageManager().isSafeMode());
- mInvariantDeviceProfile.addOnChangeListener(modelPropertiesChanged -> {
- if (modelPropertiesChanged) {
- refreshAndReloadLauncher();
- }
- });
- ThemeChangeListener themeChangeListener = this::refreshAndReloadLauncher;
- ThemeManager.INSTANCE.get(context).addChangeListener(themeChangeListener);
- mOnTerminateCallback.add(() ->
- ThemeManager.INSTANCE.get(context).removeChangeListener(themeChangeListener));
-
- ModelLauncherCallbacks callbacks = mModel.newModelCallbacks();
- LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
- launcherApps.registerCallback(callbacks);
- mOnTerminateCallback.add(() ->
- mContext.getSystemService(LauncherApps.class).unregisterCallback(callbacks));
-
- if (BuildCompat.isAtLeastV() && Flags.enableSupportForArchiving()) {
- ArchiveCompatibilityParams params = new ArchiveCompatibilityParams();
- params.setEnableUnarchivalConfirmation(false);
- params.setEnableIconOverlay(!Flags.useNewIconForArchivedApps());
- launcherApps.setArchiveCompatibility(params);
- }
-
- SimpleBroadcastReceiver modelChangeReceiver =
- new SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR, mModel::onBroadcastIntent);
- modelChangeReceiver.register(
- ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
- if (BuildConfig.IS_STUDIO_BUILD) {
- modelChangeReceiver.register(RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
- }
- mOnTerminateCallback.add(() -> modelChangeReceiver.unregisterReceiverSafely());
-
- SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
- .addUserEventListener(mModel::onUserEvent);
- mOnTerminateCallback.add(userChangeListener::close);
-
- if (enableSmartspaceRemovalToggle()) {
- OnSharedPreferenceChangeListener firstPagePinnedItemListener =
- new OnSharedPreferenceChangeListener() {
- @Override
- public void onSharedPreferenceChanged(
- SharedPreferences sharedPreferences, String key) {
- if (SMARTSPACE_ON_HOME_SCREEN.equals(key)) {
- mModel.forceReload();
- }
- }
- };
- LauncherPrefs.getPrefs(mContext).registerOnSharedPreferenceChangeListener(
- firstPagePinnedItemListener);
- mOnTerminateCallback.add(() -> LauncherPrefs.getPrefs(mContext)
- .unregisterOnSharedPreferenceChangeListener(firstPagePinnedItemListener));
- }
-
- LockedUserState.get(context).runOnUserUnlocked(() -> {
- CustomWidgetManager cwm = CustomWidgetManager.INSTANCE.get(mContext);
- mOnTerminateCallback.add(cwm.addWidgetRefreshCallback(mModel::rebindCallbacks)::close);
-
- SafeCloseable iconChangeTracker = mIconProvider.registerIconChangeListener(
- mModel::onAppIconChanged, MODEL_EXECUTOR.getHandler());
- mOnTerminateCallback.add(iconChangeTracker::close);
-
- InstallSessionTracker installSessionTracker =
- InstallSessionHelper.INSTANCE.get(context).registerInstallTracker(callbacks);
- mOnTerminateCallback.add(installSessionTracker::unregister);
- });
-
- // Register an observer to rebind the notification listener when dots are re-enabled.
- SettingsCache settingsCache = SettingsCache.INSTANCE.get(mContext);
- SettingsCache.OnChangeListener notificationLister = this::onNotificationSettingsChanged;
- settingsCache.register(NOTIFICATION_BADGING_URI, notificationLister);
- onNotificationSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI));
- mOnTerminateCallback.add(() ->
- settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
- // Register an observer to notify Launcher about Private Space settings toggle.
- registerPrivateSpaceHideWhenLockListener(settingsCache);
+ ModelInitializer initializer = new ModelInitializer(
+ context,
+ LauncherComponentProvider.get(context).getIconPool(),
+ mIconCache,
+ mInvariantDeviceProfile,
+ ThemeManager.INSTANCE.get(context),
+ UserCache.INSTANCE.get(context),
+ SettingsCache.INSTANCE.get(context),
+ mIconProvider,
+ CustomWidgetManager.INSTANCE.get(context),
+ InstallSessionHelper.INSTANCE.get(context),
+ closeable -> mOnTerminateCallback.add(closeable::close)
+ );
+ initializer.initialize(mModel);
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
@@ -186,32 +103,6 @@
mOnTerminateCallback.add(mModel::destroy);
}
- private void onNotificationSettingsChanged(boolean areNotificationDotsEnabled) {
- if (areNotificationDotsEnabled) {
- NotificationListener.requestRebind(new ComponentName(
- mContext, NotificationListener.class));
- }
- }
-
- private void registerPrivateSpaceHideWhenLockListener(SettingsCache settingsCache) {
- SettingsCache.OnChangeListener psHideWhenLockChangedListener =
- this::onPrivateSpaceHideWhenLockChanged;
- settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psHideWhenLockChangedListener);
- mOnTerminateCallback.add(() -> settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI,
- psHideWhenLockChangedListener));
- }
-
- private void onPrivateSpaceHideWhenLockChanged(boolean isPrivateSpaceHideOnLockEnabled) {
- mModel.forceReload();
- }
-
- private void refreshAndReloadLauncher() {
- LauncherIcons.clearPool(mContext);
- mIconCache.updateIconParams(
- mInvariantDeviceProfile.fillResIconDpi, mInvariantDeviceProfile.iconBitmapSize);
- mModel.forceReload();
- }
-
/**
* Call from Application.onTerminate(), which is not guaranteed to ever be called.
*/
diff --git a/src/com/android/launcher3/LauncherModel.kt b/src/com/android/launcher3/LauncherModel.kt
index 185629b..6e4276d 100644
--- a/src/com/android/launcher3/LauncherModel.kt
+++ b/src/com/android/launcher3/LauncherModel.kt
@@ -15,13 +15,11 @@
*/
package com.android.launcher3
-import android.app.admin.DevicePolicyManager
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.text.TextUtils
-import android.util.Log
import android.util.Pair
import androidx.annotation.WorkerThread
import com.android.launcher3.celllayout.CellPosMapper
@@ -47,7 +45,6 @@
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.pm.UserCache
import com.android.launcher3.shortcuts.ShortcutRequest
-import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.MODEL_EXECUTOR
import com.android.launcher3.util.PackageManagerHelper
@@ -173,15 +170,8 @@
}
}
- fun onBroadcastIntent(intent: Intent) {
- if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
- when (intent.action) {
- LauncherAppState.ACTION_FORCE_ROLOAD ->
- // If we have changed locale we need to clear out the labels in all apps/workspace.
- forceReload()
- DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED ->
- enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
- }
+ fun reloadStringCache() {
+ enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate))
}
/**
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index bff323c..6109131 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -246,10 +246,12 @@
public void setItemInfo(final ItemInfo info) {
// Load the adaptive icon on a background thread and add the view in ui thread.
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
+ ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
int w = mWidth;
int h = mHeight;
Pair<AdaptiveIconDrawable, Drawable> fullDrawable = Utilities.getFullDrawable(
- mActivity, info, w, h, true /* shouldThemeIcon */);
+ mActivity, info, w, h,
+ themeManager.isIconThemeEnabled());
if (fullDrawable != null) {
AdaptiveIconDrawable adaptiveIcon = fullDrawable.first;
int blurMargin = (int) mActivity.getResources()
@@ -274,7 +276,6 @@
Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
adaptiveIcon.setBounds(shrunkBounds);
- ThemeManager themeManager = ThemeManager.INSTANCE.get(getContext());
final Path mask = (adaptiveIcon instanceof FolderAdaptiveIcon
? themeManager.getFolderShape() : themeManager.getIconShape())
.getPath(shrunkBounds);
diff --git a/src/com/android/launcher3/model/ModelInitializer.kt b/src/com/android/launcher3/model/ModelInitializer.kt
new file mode 100644
index 0000000..69a320a
--- /dev/null
+++ b/src/com/android/launcher3/model/ModelInitializer.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.model
+
+import android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED
+import android.content.ComponentName
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.ArchiveCompatibilityParams
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.Flags
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
+import com.android.launcher3.LauncherModel
+import com.android.launcher3.LauncherPrefs.Companion.getPrefs
+import com.android.launcher3.Utilities
+import com.android.launcher3.dagger.ApplicationContext
+import com.android.launcher3.graphics.ThemeManager
+import com.android.launcher3.graphics.ThemeManager.ThemeChangeListener
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.icons.LauncherIconProvider
+import com.android.launcher3.icons.LauncherIcons.IconPool
+import com.android.launcher3.notification.NotificationListener
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.SafeCloseable
+import com.android.launcher3.util.SettingsCache
+import com.android.launcher3.util.SettingsCache.NOTIFICATION_BADGING_URI
+import com.android.launcher3.util.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI
+import com.android.launcher3.util.SimpleBroadcastReceiver
+import com.android.launcher3.widget.custom.CustomWidgetManager
+import java.util.function.Consumer
+
+/** Utility class for initializing all model callbacks */
+class ModelInitializer(
+ @ApplicationContext private val context: Context,
+ private val iconPool: IconPool,
+ private val iconCache: IconCache,
+ private val idp: InvariantDeviceProfile,
+ private val themeManager: ThemeManager,
+ private val userCache: UserCache,
+ private val settingsCache: SettingsCache,
+ private val iconProvider: LauncherIconProvider,
+ private val customWidgetManager: CustomWidgetManager,
+ private val installSessionHelper: InstallSessionHelper,
+ private val closeActions: Consumer<SafeCloseable>,
+) {
+
+ fun initialize(model: LauncherModel) {
+ fun refreshAndReloadLauncher() {
+ iconPool.clear()
+ iconCache.updateIconParams(idp.fillResIconDpi, idp.iconBitmapSize)
+ model.forceReload()
+ }
+
+ // IDP changes
+ val idpChangeListener = OnIDPChangeListener { modelChanged ->
+ if (modelChanged) refreshAndReloadLauncher()
+ }
+ idp.addOnChangeListener(idpChangeListener)
+ closeActions.accept { idp.removeOnChangeListener(idpChangeListener) }
+
+ // Theme changes
+ val themeChangeListener = ThemeChangeListener { refreshAndReloadLauncher() }
+ themeManager.addChangeListener(themeChangeListener)
+ closeActions.accept { themeManager.removeChangeListener(themeChangeListener) }
+
+ // System changes
+ val modelCallbacks = model.newModelCallbacks()
+ val launcherApps = context.getSystemService(LauncherApps::class.java)!!
+ launcherApps.registerCallback(modelCallbacks)
+ closeActions.accept { launcherApps.unregisterCallback(modelCallbacks) }
+
+ if (Utilities.ATLEAST_V && Flags.enableSupportForArchiving()) {
+ launcherApps.setArchiveCompatibility(
+ ArchiveCompatibilityParams().apply {
+ setEnableUnarchivalConfirmation(false)
+ setEnableIconOverlay(!Flags.useNewIconForArchivedApps())
+ }
+ )
+ }
+
+ // Device profile policy changes
+ val dpUpdateReceiver =
+ SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.reloadStringCache() }
+ dpUpdateReceiver.register(ACTION_DEVICE_POLICY_RESOURCE_UPDATED)
+ closeActions.accept { dpUpdateReceiver.unregisterReceiverSafely() }
+
+ // Development helper
+ if (BuildConfig.IS_STUDIO_BUILD) {
+ val reloadReceiver =
+ SimpleBroadcastReceiver(context, UI_HELPER_EXECUTOR) { model.forceReload() }
+ reloadReceiver.register(Context.RECEIVER_EXPORTED, ACTION_FORCE_RELOAD)
+ closeActions.accept { reloadReceiver.unregisterReceiverSafely() }
+ }
+
+ // User changes
+ closeActions.accept(userCache.addUserEventListener(model::onUserEvent))
+
+ // Private space settings changes
+ val psSettingsListener = SettingsCache.OnChangeListener { model.forceReload() }
+ settingsCache.register(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+ closeActions.accept {
+ settingsCache.unregister(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, psSettingsListener)
+ }
+
+ // Notification dots changes
+ val notificationChanges =
+ SettingsCache.OnChangeListener { dotsEnabled ->
+ if (dotsEnabled)
+ NotificationListener.requestRebind(
+ ComponentName(context, NotificationListener::class.java)
+ )
+ }
+ settingsCache.register(NOTIFICATION_BADGING_URI, notificationChanges)
+ notificationChanges.onSettingsChanged(settingsCache.getValue(NOTIFICATION_BADGING_URI))
+ closeActions.accept {
+ settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationChanges)
+ }
+
+ // removable smartspace
+ if (Flags.enableSmartspaceRemovalToggle()) {
+ val smartSpacePrefChanges =
+ SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (LoaderTask.SMARTSPACE_ON_HOME_SCREEN == key) model.forceReload()
+ }
+ getPrefs(context).registerOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+ closeActions.accept {
+ getPrefs(context).unregisterOnSharedPreferenceChangeListener(smartSpacePrefChanges)
+ }
+ }
+
+ // Custom widgets
+ closeActions.accept(customWidgetManager.addWidgetRefreshCallback(model::rebindCallbacks))
+
+ // Icon changes
+ closeActions.accept(
+ iconProvider.registerIconChangeListener(model::onAppIconChanged, MODEL_EXECUTOR.handler)
+ )
+
+ // Install session changes
+ closeActions.accept(installSessionHelper.registerInstallTracker(modelCallbacks))
+ }
+
+ companion object {
+ private const val ACTION_FORCE_RELOAD = "force-reload-launcher"
+ }
+}
diff --git a/src/com/android/launcher3/pm/InstallSessionTracker.java b/src/com/android/launcher3/pm/InstallSessionTracker.java
index b9c928c..7451ce2 100644
--- a/src/com/android/launcher3/pm/InstallSessionTracker.java
+++ b/src/com/android/launcher3/pm/InstallSessionTracker.java
@@ -34,13 +34,15 @@
import com.android.launcher3.Flags;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.SafeCloseable;
import java.lang.ref.WeakReference;
import java.util.Objects;
@SuppressWarnings("NewApi")
@WorkerThread
-public class InstallSessionTracker extends PackageInstaller.SessionCallback {
+public class InstallSessionTracker extends PackageInstaller.SessionCallback implements
+ SafeCloseable {
public static final String TAG = "InstallSessionTracker";
@@ -196,7 +198,8 @@
}
}
- public void unregister() {
+ @Override
+ public void close() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
mInstaller.unregisterSessionCallback(this);
} else {
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
index 15a9964..23c1da9 100644
--- a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionTrackerTest.kt
@@ -219,7 +219,7 @@
.whenever(launcherApps)
.unregisterPackageInstallerSessionCallback(installSessionTracker)
// When
- installSessionTracker.unregister()
+ installSessionTracker.close()
// Then
verify(launcherApps).unregisterPackageInstallerSessionCallback(installSessionTracker)
}