Refactor ShortcutsChangedTask to Kotlin
Bug: 375414891
Test: ShortcutsChangedTaskTest
Flag: EXEMPT no functionality change
Change-Id: If80b74a80abf9bed90f1572a9fee2494d39a6829
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
deleted file mode 100644
index b5a7382..0000000
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2016 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.content.Context;
-import android.content.pm.ShortcutInfo;
-import android.os.UserHandle;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.ModelUpdateTask;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.icons.CacheableShortcutInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.ApplicationInfoWrapper;
-import com.android.launcher3.util.ItemInfoMatcher;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Handles changes due to shortcut manager updates (deep shortcut changes)
- */
-public class ShortcutsChangedTask implements ModelUpdateTask {
-
- @NonNull
- private final String mPackageName;
-
- @NonNull
- private final List<ShortcutInfo> mShortcuts;
-
- @NonNull
- private final UserHandle mUser;
-
- private final boolean mUpdateIdMap;
-
- public ShortcutsChangedTask(@NonNull final String packageName,
- @NonNull final List<ShortcutInfo> shortcuts, @NonNull final UserHandle user,
- final boolean updateIdMap) {
- mPackageName = packageName;
- mShortcuts = shortcuts;
- mUser = user;
- mUpdateIdMap = updateIdMap;
- }
-
- @Override
- public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
- @NonNull AllAppsList apps) {
- final LauncherAppState app = taskController.getApp();
- final Context context = app.getContext();
- // Find WorkspaceItemInfo's that have changed on the workspace.
- ArrayList<WorkspaceItemInfo> matchingWorkspaceItems = new ArrayList<>();
-
- synchronized (dataModel) {
- dataModel.forAllWorkspaceItemInfos(mUser, si -> {
- if ((si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
- && mPackageName.equals(si.getIntent().getPackage())) {
- matchingWorkspaceItems.add(si);
- }
- });
- }
-
- if (!matchingWorkspaceItems.isEmpty()) {
- ApplicationInfoWrapper infoWrapper =
- new ApplicationInfoWrapper(context, mPackageName, mUser);
- if (mShortcuts.isEmpty()) {
- // Verify that the app is indeed installed.
- if (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) {
- // App is not installed or archived, ignoring package events
- return;
- }
- }
- // Update the workspace to reflect the changes to updated shortcuts residing on it.
- List<String> allLauncherKnownIds = matchingWorkspaceItems.stream()
- .map(WorkspaceItemInfo::getDeepShortcutId)
- .distinct()
- .collect(Collectors.toList());
- List<ShortcutInfo> shortcuts = new ShortcutRequest(context, mUser)
- .forPackage(mPackageName, allLauncherKnownIds)
- .query(ShortcutRequest.ALL);
-
- Set<String> nonPinnedIds = new HashSet<>(allLauncherKnownIds);
- ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
- for (ShortcutInfo fullDetails : shortcuts) {
- if (!fullDetails.isPinned()) {
- continue;
- }
- String sid = fullDetails.getId();
- nonPinnedIds.remove(sid);
- matchingWorkspaceItems
- .stream()
- .filter(itemInfo -> sid.equals(itemInfo.getDeepShortcutId()))
- .forEach(workspaceItemInfo -> {
- workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context);
- app.getIconCache().getShortcutIcon(workspaceItemInfo,
- new CacheableShortcutInfo(fullDetails, infoWrapper));
- updatedWorkspaceItemInfos.add(workspaceItemInfo);
- });
- }
-
- taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos);
- if (!nonPinnedIds.isEmpty()) {
- taskController.deleteAndBindComponentsRemoved(ItemInfoMatcher.ofShortcutKeys(
- nonPinnedIds.stream()
- .map(id -> new ShortcutKey(mPackageName, mUser, id))
- .collect(Collectors.toSet())),
- "removed because the shortcut is no longer available in shortcut service");
- }
- }
-
- if (mUpdateIdMap) {
- // Update the deep shortcut map if the list of ids has changed for an activity.
- dataModel.updateDeepShortcutCounts(mPackageName, mUser, mShortcuts);
- taskController.bindDeepShortcuts(dataModel);
- }
- }
-}
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.kt b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
new file mode 100644
index 0000000..2e4f75f
--- /dev/null
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.content.pm.ShortcutInfo
+import android.os.UserHandle
+import com.android.launcher3.LauncherModel.ModelUpdateTask
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.icons.CacheableShortcutInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.shortcuts.ShortcutRequest
+import com.android.launcher3.util.ApplicationInfoWrapper
+import com.android.launcher3.util.ItemInfoMatcher
+
+/** Handles changes due to shortcut manager updates (deep shortcut changes) */
+class ShortcutsChangedTask(
+ private val packageName: String,
+ private val shortcuts: List<ShortcutInfo>,
+ private val user: UserHandle,
+ private val shouldUpdateIdMap: Boolean,
+) : ModelUpdateTask {
+
+ override fun execute(
+ taskController: ModelTaskController,
+ dataModel: BgDataModel,
+ apps: AllAppsList,
+ ) {
+ val app = taskController.app
+ val context = app.context
+ // Find WorkspaceItemInfo's that have changed on the workspace.
+ val matchingWorkspaceItems = ArrayList<WorkspaceItemInfo>()
+
+ synchronized(dataModel) {
+ dataModel.forAllWorkspaceItemInfos(user) { wai: WorkspaceItemInfo ->
+ if (
+ (wai.itemType == ITEM_TYPE_DEEP_SHORTCUT) &&
+ packageName == wai.getIntent().getPackage()
+ ) {
+ matchingWorkspaceItems.add(wai)
+ }
+ }
+ }
+
+ if (matchingWorkspaceItems.isNotEmpty()) {
+ val infoWrapper = ApplicationInfoWrapper(context, packageName, user)
+ if (shortcuts.isEmpty()) {
+ // Verify that the app is indeed installed.
+ if (!infoWrapper.isInstalled() && !infoWrapper.isArchived()) {
+ // App is not installed or archived, ignoring package events
+ return
+ }
+ }
+ // Update the workspace to reflect the changes to updated shortcuts residing on it.
+ val allLauncherKnownIds =
+ matchingWorkspaceItems.map { item -> item.deepShortcutId }.distinct()
+ val shortcuts: List<ShortcutInfo> =
+ ShortcutRequest(context, user)
+ .forPackage(packageName, allLauncherKnownIds)
+ .query(ShortcutRequest.ALL)
+
+ val nonPinnedIds: MutableSet<String> = HashSet(allLauncherKnownIds)
+ val updatedWorkspaceItemInfos = ArrayList<WorkspaceItemInfo>()
+ for (fullDetails in shortcuts) {
+ if (!fullDetails.isPinned) {
+ continue
+ }
+ val shortcutId = fullDetails.id
+ nonPinnedIds.remove(shortcutId)
+ matchingWorkspaceItems
+ .filter { itemInfo: WorkspaceItemInfo -> shortcutId == itemInfo.deepShortcutId }
+ .forEach { workspaceItemInfo: WorkspaceItemInfo ->
+ workspaceItemInfo.updateFromDeepShortcutInfo(fullDetails, context)
+ app.iconCache.getShortcutIcon(
+ workspaceItemInfo,
+ CacheableShortcutInfo(fullDetails, infoWrapper),
+ )
+ updatedWorkspaceItemInfos.add(workspaceItemInfo)
+ }
+ }
+
+ taskController.bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos)
+ if (nonPinnedIds.isNotEmpty()) {
+ taskController.deleteAndBindComponentsRemoved(
+ ItemInfoMatcher.ofShortcutKeys(
+ nonPinnedIds
+ .map { id: String? -> ShortcutKey(packageName, user, id) }
+ .toSet()
+ ),
+ "removed because the shortcut is no longer available in shortcut service",
+ )
+ }
+ }
+
+ if (shouldUpdateIdMap) {
+ // Update the deep shortcut map if the list of ids has changed for an activity.
+ dataModel.updateDeepShortcutCounts(packageName, user, shortcuts)
+ taskController.bindDeepShortcuts(dataModel)
+ }
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
new file mode 100644
index 0000000..fb6d038
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/model/ShortcutsChangedTaskTest.kt
@@ -0,0 +1,313 @@
+/*
+ * 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.content.ComponentName
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutInfo
+import android.os.Process.myUserHandle
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.icons.BitmapInfo
+import com.android.launcher3.icons.CacheableShortcutInfo
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.IntSparseArrayMap
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Predicate
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutsChangedTaskTest {
+ private lateinit var shortcutsChangedTask: ShortcutsChangedTask
+ private lateinit var modelHelper: LauncherModelHelper
+ private lateinit var context: SandboxModelContext
+ private lateinit var launcherApps: LauncherApps
+ private var shortcuts: List<ShortcutInfo> = emptyList()
+
+ private val expectedPackage: String = "expected"
+ private val expectedShortcutId: String = "shortcut_id"
+ private val user: UserHandle = myUserHandle()
+ private val mockTaskController: ModelTaskController = mock()
+ private val mockAllApps: AllAppsList = mock()
+ private val mockAppState: LauncherAppState = mock()
+ private val mockIconCache: IconCache = mock()
+
+ private val expectedWai =
+ WorkspaceItemInfo().apply {
+ id = 1
+ itemType = ITEM_TYPE_DEEP_SHORTCUT
+ intent =
+ Intent().apply {
+ `package` = expectedPackage
+ putExtra(ShortcutKey.EXTRA_SHORTCUT_ID, expectedShortcutId)
+ }
+ }
+
+ @Before
+ fun setup() {
+ modelHelper = LauncherModelHelper()
+ modelHelper.loadModelSync()
+ context = modelHelper.sandboxContext
+ launcherApps = context.spyService(LauncherApps::class.java)
+ whenever(mockTaskController.app).thenReturn(mockAppState)
+ whenever(mockAppState.context).thenReturn(context)
+ whenever(mockAppState.iconCache).thenReturn(mockIconCache)
+ whenever(mockIconCache.getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>()))
+ .then { _ -> { expectedWai.bitmap = BitmapInfo.LOW_RES_INFO } }
+ shortcuts = emptyList()
+ shortcutsChangedTask = ShortcutsChangedTask(expectedPackage, shortcuts, user, false)
+ }
+
+ @After
+ fun teardown() {
+ modelHelper.destroy()
+ }
+
+ @Test
+ fun `When installed pinned shortcut is found then keep in workspace`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(true)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags or FLAG_INSTALLED
+ isArchived = false
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockAppState.iconCache)
+ .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+ verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
+ }
+
+ @Test
+ fun `When installed unpinned shortcut is found then remove from workspace`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(false)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags or FLAG_INSTALLED
+ isArchived = false
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockTaskController)
+ .deleteAndBindComponentsRemoved(
+ any<Predicate<ItemInfo?>>(),
+ eq("removed because the shortcut is no longer available in shortcut service"),
+ )
+ }
+
+ @Test
+ fun `When shortcut app is uninstalled then skip handling`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(true)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags and FLAG_INSTALLED.inv()
+ isArchived = false
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockTaskController, times(0)).deleteAndBindComponentsRemoved(any(), any())
+ verify(mockTaskController, times(0)).bindUpdatedWorkspaceItems(any())
+ }
+
+ @Test
+ fun `When archived pinned shortcut is found then keep in workspace`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(true)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags or FLAG_INSTALLED
+ isArchived = true
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockAppState.iconCache)
+ .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+ verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
+ }
+
+ @Test
+ fun `When archived unpinned shortcut is found then keep in workspace`() {
+ // Given
+ shortcuts =
+ listOf(
+ mock<ShortcutInfo>().apply {
+ whenever(isPinned).thenReturn(true)
+ whenever(id).thenReturn(expectedShortcutId)
+ }
+ )
+ val items: IntSparseArrayMap<ItemInfo> = modelHelper.bgDataModel.itemsIdMap
+ items.put(expectedWai.id, expectedWai)
+ doReturn(
+ ApplicationInfo().apply {
+ enabled = true
+ flags = flags or FLAG_INSTALLED
+ isArchived = true
+ }
+ )
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ doReturn(shortcuts).whenever(launcherApps).getShortcuts(any(), eq(user))
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ verify(mockAppState.iconCache)
+ .getShortcutIcon(eq(expectedWai), any<CacheableShortcutInfo>())
+ verify(mockTaskController).bindUpdatedWorkspaceItems(listOf(expectedWai))
+ }
+
+ @Test
+ fun `When updateIdMap true then trigger deep shortcut binding`() {
+ // Given
+ val expectedShortcut =
+ mock<ShortcutInfo>().apply {
+ whenever(isEnabled).thenReturn(true)
+ whenever(isDeclaredInManifest).thenReturn(true)
+ whenever(activity).thenReturn(ComponentName(expectedPackage, "expectedClass"))
+ whenever(id).thenReturn(expectedShortcutId)
+ whenever(userHandle).thenReturn(user)
+ }
+ shortcuts = listOf(expectedShortcut)
+ val expectedKey = ComponentKey(expectedShortcut.activity, expectedShortcut.userHandle)
+ doReturn(ApplicationInfo())
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ shortcutsChangedTask =
+ ShortcutsChangedTask(
+ packageName = expectedPackage,
+ shortcuts = shortcuts,
+ user = user,
+ shouldUpdateIdMap = true,
+ )
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ assertThat(modelHelper.bgDataModel.deepShortcutMap).containsEntry(expectedKey, 1)
+ verify(mockTaskController).bindDeepShortcuts(eq(modelHelper.bgDataModel))
+ }
+
+ @Test
+ fun `When updateIdMap false then do not trigger deep shortcut binding`() {
+ // Given
+ val expectedShortcut =
+ mock<ShortcutInfo>().apply {
+ whenever(isEnabled).thenReturn(true)
+ whenever(isDeclaredInManifest).thenReturn(true)
+ whenever(activity).thenReturn(ComponentName(expectedPackage, "expectedClass"))
+ whenever(id).thenReturn(expectedShortcutId)
+ whenever(userHandle).thenReturn(user)
+ }
+ shortcuts = listOf(expectedShortcut)
+ val expectedKey = ComponentKey(expectedShortcut.activity, expectedShortcut.userHandle)
+ doReturn(ApplicationInfo())
+ .whenever(launcherApps)
+ .getApplicationInfo(eq(expectedPackage), any(), eq(user))
+ shortcutsChangedTask =
+ ShortcutsChangedTask(
+ packageName = expectedPackage,
+ shortcuts = shortcuts,
+ user = user,
+ shouldUpdateIdMap = false,
+ )
+ // When
+ shortcutsChangedTask.execute(mockTaskController, modelHelper.bgDataModel, mockAllApps)
+ // Then
+ assertThat(modelHelper.bgDataModel.deepShortcutMap).doesNotContainKey(expectedKey)
+ verify(mockTaskController, times(0)).bindDeepShortcuts(eq(modelHelper.bgDataModel))
+ }
+}