Merge "Cleaning up release flag: ENABLE_ICON_LABEL_AUTO_SCALING" into main
diff --git a/res/layout/widgets_two_pane_sheet_paged_view.xml b/res/layout/widgets_two_pane_sheet_paged_view.xml
index 442957a..4a7749b 100644
--- a/res/layout/widgets_two_pane_sheet_paged_view.xml
+++ b/res/layout/widgets_two_pane_sheet_paged_view.xml
@@ -66,7 +66,7 @@
                 <include layout="@layout/widgets_search_bar" />
             </FrameLayout>
 
-            <LinearLayout
+            <FrameLayout
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:id="@+id/suggestions_header"
@@ -74,7 +74,7 @@
                 android:orientation="horizontal"
                 android:background="?attr/widgetPickerPrimarySurfaceColor"
                 launcher:layout_sticky="true">
-            </LinearLayout>
+            </FrameLayout>
 
             <com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
                 android:id="@+id/tabs"
diff --git a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
index f98cab6..44c41c1 100644
--- a/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
+++ b/src/com/android/launcher3/model/WorkspaceItemProcessor.kt
@@ -26,7 +26,6 @@
 import android.text.TextUtils
 import android.util.Log
 import android.util.LongSparseArray
-import androidx.annotation.VisibleForTesting
 import com.android.launcher3.Flags
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherAppState
@@ -89,7 +88,10 @@
         try {
             if (c.user == null) {
                 // User has been deleted, remove the item.
-                c.markDeleted("User has been deleted", RestoreError.PROFILE_DELETED)
+                c.markDeleted(
+                    "User has been deleted for item id=${c.id}",
+                    RestoreError.PROFILE_DELETED
+                )
                 return
             }
             when (c.itemType) {
@@ -127,29 +129,24 @@
      * data model to be bound to the launcher’s data model.
      */
     @SuppressLint("NewApi")
-    @VisibleForTesting
-    fun processAppOrDeepShortcut() {
+    private fun processAppOrDeepShortcut() {
         var allowMissingTarget = false
         var intent = c.parseIntent()
         if (intent == null) {
-            c.markDeleted("Invalid or null intent", RestoreError.MISSING_INFO)
+            c.markDeleted("Null intent for item id=${c.id}", RestoreError.MISSING_INFO)
             return
         }
         var disabledState =
             if (userManagerState.isUserQuiet(c.serialNumber))
                 WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER
             else 0
-        var cn = intent.component
-        val targetPkg = if (cn == null) intent.getPackage() else cn.packageName
-        if (TextUtils.isEmpty(targetPkg)) {
-            c.markDeleted("Shortcuts can't have null package", RestoreError.MISSING_INFO)
+        val cn = intent.component
+        val targetPkg = cn?.packageName ?: intent.getPackage()
+        if (targetPkg.isNullOrEmpty()) {
+            c.markDeleted("No target package for item id=${c.id}", RestoreError.MISSING_INFO)
             return
         }
-
-        // If there is no target package, it's an implicit intent
-        // (legacy shortcut) which is always valid
-        var validTarget =
-            (TextUtils.isEmpty(targetPkg) || launcherApps.isPackageEnabled(targetPkg, c.user))
+        var validTarget = launcherApps.isPackageEnabled(targetPkg, c.user)
 
         // If it's a deep shortcut, we'll use pinned shortcuts to restore it
         if (cn != null && validTarget && (c.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT)) {
@@ -359,8 +356,7 @@
      * processing for folder content items is done in LoaderTask after all the items in the
      * workspace have been loaded. The loaded FolderInfos are stored in the BgDataModel.
      */
-    @VisibleForTesting
-    fun processFolderOrAppPair() {
+    private fun processFolderOrAppPair() {
         val folderInfo =
             bgDataModel.findOrMakeFolder(c.id).apply {
                 c.applyCommonProperties(this)
@@ -394,8 +390,7 @@
      * depending on the type of widget. Custom widgets are treated differently than non-custom
      * widgets, installing / restoring widgets are treated differently, etc.
      */
-    @VisibleForTesting
-    fun processWidget() {
+    private fun processWidget() {
         val component = ComponentName.unflattenFromString(c.appWidgetProvider)!!
         val appWidgetInfo = LauncherAppWidgetInfo(c.appWidgetId, component)
         c.applyCommonProperties(appWidgetInfo)
@@ -496,6 +491,7 @@
 
     companion object {
         private const val TAG = "WorkspaceItemProcessor"
+
         private fun logWidgetInfo(
             idp: InvariantDeviceProfile,
             widgetProviderInfo: LauncherAppWidgetProviderInfo
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestConstants.java b/tests/multivalentTests/src/com/android/launcher3/util/TestConstants.java
index 6f3c63a..bf5ccd0 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestConstants.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestConstants.java
@@ -23,6 +23,7 @@
         public static final String MAPS_APP_NAME = "Maps";
         public static final String STORE_APP_NAME = "Play Store";
         public static final String GMAIL_APP_NAME = "Gmail";
+        public static final String PHOTOS_APP_NAME = "Photos";
         public static final String CHROME_APP_NAME = "Chrome";
         public static final String MESSAGES_APP_NAME = "Messages";
     }
diff --git a/tests/src/com/android/launcher3/dragging/TaplDragTest.java b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
index 94a96aa..b633452 100644
--- a/tests/src/com/android/launcher3/dragging/TaplDragTest.java
+++ b/tests/src/com/android/launcher3/dragging/TaplDragTest.java
@@ -15,11 +15,11 @@
  */
 package com.android.launcher3.dragging;
 
-import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.GMAIL_APP_NAME;
+import static com.android.launcher3.util.TestConstants.AppNames.PHOTOS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.MAPS_APP_NAME;
 import static com.android.launcher3.util.TestConstants.AppNames.STORE_APP_NAME;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.initialize;
+import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 
@@ -80,18 +80,18 @@
         // TODO: add the use case to drag an icon to an existing folder. Currently it either fails
         // on tablets or phones due to difference in resolution.
         final HomeAppIcon playStoreIcon = createShortcutIfNotExist(STORE_APP_NAME, 0, 1);
-        final HomeAppIcon gmailIcon = createShortcutInCenterIfNotExist(GMAIL_APP_NAME);
+        final HomeAppIcon photosIcon = createShortcutInCenterIfNotExist(PHOTOS_APP_NAME);
 
-        FolderIcon folderIcon = gmailIcon.dragToIcon(playStoreIcon);
+        FolderIcon folderIcon = photosIcon.dragToIcon(playStoreIcon);
         Folder folder = folderIcon.open();
         folder.getAppIcon(STORE_APP_NAME);
-        folder.getAppIcon(GMAIL_APP_NAME);
+        folder.getAppIcon(PHOTOS_APP_NAME);
         Workspace workspace = folder.close();
 
         workspace.verifyWorkspaceAppIconIsGone(STORE_APP_NAME + " should be moved to a folder.",
                 STORE_APP_NAME);
-        workspace.verifyWorkspaceAppIconIsGone(GMAIL_APP_NAME + " should be moved to a folder.",
-                GMAIL_APP_NAME);
+        workspace.verifyWorkspaceAppIconIsGone(PHOTOS_APP_NAME + " should be moved to a folder.",
+                PHOTOS_APP_NAME);
 
         final HomeAppIcon mapIcon = createShortcutInCenterIfNotExist(MAPS_APP_NAME);
         folderIcon = mapIcon.dragToIcon(folderIcon);
diff --git a/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
new file mode 100644
index 0000000..e94dc02
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/WorkspaceItemProcessorTest.kt
@@ -0,0 +1,290 @@
+/*
+ * 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.model
+
+import android.appwidget.AppWidgetProviderInfo
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.ShortcutInfo
+import android.os.UserHandle
+import android.util.LongSparseArray
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.MISSING_INFO
+import com.android.launcher3.backuprestore.LauncherRestoreEventLogger.RestoreError.Companion.PROFILE_DELETED
+import com.android.launcher3.model.data.IconRequestInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.shortcuts.ShortcutKey
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.WidgetInflater
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.RETURNS_DEEP_STUBS
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class WorkspaceItemProcessorTest {
+    private var itemProcessor = createTestWorkspaceItemProcessor()
+
+    @Before
+    fun setup() {
+        itemProcessor = createTestWorkspaceItemProcessor()
+    }
+
+    @Test
+    fun `When user is null then mark item deleted`() {
+        // Given
+        val mockCursor = mock<LoaderCursor>().apply { id = 1 }
+        val itemProcessor = createTestWorkspaceItemProcessor(cursor = mockCursor)
+        // When
+        itemProcessor.processItem()
+        // Then
+        verify(mockCursor).markDeleted("User has been deleted for item id=1", PROFILE_DELETED)
+    }
+
+    @Test
+    fun `When app has null intent then mark deleted`() {
+        // Given
+        val mockCursor =
+            mock<LoaderCursor>().apply {
+                user = UserHandle(0)
+                id = 1
+                itemType = ITEM_TYPE_APPLICATION
+            }
+        val itemProcessor = createTestWorkspaceItemProcessor(cursor = mockCursor)
+        // When
+        itemProcessor.processItem()
+        // Then
+        verify(mockCursor).markDeleted("Null intent for item id=1", MISSING_INFO)
+    }
+
+    @Test
+    fun `When app has null target package then mark deleted`() {
+        // Given
+        val mockCursor =
+            mock<LoaderCursor>().apply {
+                user = UserHandle(0)
+                itemType = ITEM_TYPE_APPLICATION
+                id = 1
+                whenever(parseIntent()).thenReturn(Intent())
+            }
+        val itemProcessor = createTestWorkspaceItemProcessor(cursor = mockCursor)
+        // When
+        itemProcessor.processItem()
+        // Then
+        verify(mockCursor).markDeleted("No target package for item id=1", MISSING_INFO)
+    }
+
+    @Test
+    fun `When app has empty String target package then mark deleted`() {
+        // Given
+        val mockIntent =
+            mock<Intent>().apply {
+                whenever(component).thenReturn(null)
+                whenever(`package`).thenReturn("")
+            }
+        val mockCursor =
+            mock<LoaderCursor>().apply {
+                user = UserHandle(0)
+                itemType = ITEM_TYPE_APPLICATION
+                id = 1
+                whenever(parseIntent()).thenReturn(mockIntent)
+            }
+        val itemProcessor = createTestWorkspaceItemProcessor(cursor = mockCursor)
+        // When
+        itemProcessor.processItem()
+        // Then
+        verify(mockCursor).markDeleted("No target package for item id=1", MISSING_INFO)
+    }
+
+    @Test
+    fun `When valid app then mark restored`() {
+        // Given
+        val userHandle = UserHandle(0)
+        val componentName = ComponentName("package", "class")
+        val mockIntent =
+            mock<Intent>().apply {
+                whenever(component).thenReturn(componentName)
+                whenever(`package`).thenReturn("")
+            }
+        val mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", userHandle)).thenReturn(true)
+                whenever(isActivityEnabled(componentName, userHandle)).thenReturn(true)
+            }
+        val mockCursor =
+            mock<LoaderCursor>().apply {
+                user = userHandle
+                itemType = ITEM_TYPE_APPLICATION
+                id = 1
+                restoreFlag = 1
+                whenever(parseIntent()).thenReturn(mockIntent)
+                whenever(markRestored()).doAnswer { restoreFlag = 0 }
+            }
+        val itemProcessor =
+            createTestWorkspaceItemProcessor(cursor = mockCursor, launcherApps = mockLauncherApps)
+        // When
+        itemProcessor.processItem()
+        // Then
+        assertWithMessage("item restoreFlag should be set to 0")
+            .that(mockCursor.restoreFlag)
+            .isEqualTo(0)
+        // currently gets marked restored twice, although markRestore() has check for restoreFlag
+        verify(mockCursor, times(2)).markRestored()
+    }
+
+    @Test
+    fun `When fallback Activity found for app then mark restored`() {
+        // Given
+        val userHandle = UserHandle(0)
+        val componentName = ComponentName("package", "class")
+        val mockIntent =
+            mock<Intent>().apply {
+                whenever(component).thenReturn(componentName)
+                whenever(`package`).thenReturn("")
+                whenever(toUri(0)).thenReturn("")
+            }
+        val mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", userHandle)).thenReturn(true)
+                whenever(isActivityEnabled(componentName, userHandle)).thenReturn(false)
+            }
+        val mockPmHelper =
+            mock<PackageManagerHelper>().apply {
+                whenever(getAppLaunchIntent(componentName.packageName, userHandle))
+                    .thenReturn(mockIntent)
+            }
+        val mockCursor =
+            mock(LoaderCursor::class.java, RETURNS_DEEP_STUBS).apply {
+                user = userHandle
+                itemType = ITEM_TYPE_APPLICATION
+                id = 1
+                restoreFlag = 1
+                whenever(parseIntent()).thenReturn(mockIntent)
+                whenever(markRestored()).doAnswer { restoreFlag = 0 }
+                whenever(updater().put(Favorites.INTENT, mockIntent.toUri(0)).commit())
+                    .thenReturn(1)
+            }
+        val itemProcessor =
+            createTestWorkspaceItemProcessor(
+                cursor = mockCursor,
+                launcherApps = mockLauncherApps,
+                pmHelper = mockPmHelper
+            )
+        // When
+        itemProcessor.processItem()
+        // Then
+        assertWithMessage("item restoreFlag should be set to 0")
+            .that(mockCursor.restoreFlag)
+            .isEqualTo(0)
+        verify(mockCursor.updater().put(Favorites.INTENT, mockIntent.toUri(0))).commit()
+    }
+
+    @Test
+    fun `When app with disabled activity and no fallback found then mark deleted`() {
+        // Given
+        val userHandle = UserHandle(0)
+        val componentName = ComponentName("package", "class")
+        val mockIntent =
+            mock<Intent>().apply {
+                whenever(component).thenReturn(componentName)
+                whenever(`package`).thenReturn("")
+            }
+        val mockLauncherApps =
+            mock<LauncherApps>().apply {
+                whenever(isPackageEnabled("package", userHandle)).thenReturn(true)
+                whenever(isActivityEnabled(componentName, userHandle)).thenReturn(false)
+            }
+        val mockPmHelper =
+            mock<PackageManagerHelper>().apply {
+                whenever(getAppLaunchIntent(componentName.packageName, userHandle)).thenReturn(null)
+            }
+        val mockCursor =
+            mock<LoaderCursor>().apply {
+                user = userHandle
+                itemType = ITEM_TYPE_APPLICATION
+                id = 1
+                restoreFlag = 1
+                whenever(parseIntent()).thenReturn(mockIntent)
+            }
+        val itemProcessor =
+            createTestWorkspaceItemProcessor(
+                cursor = mockCursor,
+                launcherApps = mockLauncherApps,
+                pmHelper = mockPmHelper
+            )
+        // When
+        itemProcessor.processItem()
+        // Then
+        assertWithMessage("item restoreFlag should be unchanged")
+            .that(mockCursor.restoreFlag)
+            .isEqualTo(1)
+        verify(mockCursor).markDeleted("Intent null, unable to find a launch target", MISSING_INFO)
+    }
+
+    /**
+     * Helper to create WorkspaceItemProcessor with defaults. WorkspaceItemProcessor has a lot of
+     * dependencies, so this method can be used to inject concrete arguments while keeping the rest
+     * as mocks/defaults.
+     */
+    private fun createTestWorkspaceItemProcessor(
+        cursor: LoaderCursor = mock(),
+        memoryLogger: LoaderMemoryLogger? = null,
+        userManagerState: UserManagerState = mock(),
+        launcherApps: LauncherApps = mock(),
+        shortcutKeyToPinnedShortcuts: Map<ShortcutKey, ShortcutInfo> = mapOf(),
+        app: LauncherAppState = mock(),
+        bgDataModel: BgDataModel = mock(),
+        widgetProvidersMap: MutableMap<ComponentKey, AppWidgetProviderInfo?> = mutableMapOf(),
+        widgetInflater: WidgetInflater = mock(),
+        pmHelper: PackageManagerHelper = mock(),
+        iconRequestInfos: MutableList<IconRequestInfo<WorkspaceItemInfo>> = mutableListOf(),
+        isSdCardReady: Boolean = false,
+        pendingPackages: MutableSet<PackageUserKey> = mutableSetOf(),
+        unlockedUsers: LongSparseArray<Boolean> = LongSparseArray(),
+        installingPkgs: HashMap<PackageUserKey, PackageInstaller.SessionInfo> = hashMapOf(),
+        allDeepShortcuts: MutableList<ShortcutInfo> = mutableListOf()
+    ) =
+        WorkspaceItemProcessor(
+            c = cursor,
+            memoryLogger = memoryLogger,
+            userManagerState = userManagerState,
+            launcherApps = launcherApps,
+            app = app,
+            bgDataModel = bgDataModel,
+            widgetProvidersMap = widgetProvidersMap,
+            widgetInflater = widgetInflater,
+            pmHelper = pmHelper,
+            unlockedUsers = unlockedUsers,
+            iconRequestInfos = iconRequestInfos,
+            pendingPackages = pendingPackages,
+            isSdCardReady = isSdCardReady,
+            shortcutKeyToPinnedShortcuts = shortcutKeyToPinnedShortcuts,
+            installingPkgs = installingPkgs,
+            allDeepShortcuts = allDeepShortcuts
+        )
+}
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index db38c68..ec226af 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -15,6 +15,9 @@
  */
 package com.android.launcher3.ui.widget;
 
+import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
+import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
+
 import static org.junit.Assert.assertNotNull;
 
 import android.platform.test.annotations.PlatinumTest;
@@ -30,10 +33,11 @@
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.util.rule.TestStabilityRule.Stability;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
+import org.junit.Assume;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -89,10 +93,13 @@
      * A custom shortcut is a 1x1 widget that launches a specific intent when user tap on it.
      * Custom shortcuts are replaced by deep shortcuts after api 25.
      */
-    @Ignore
+    @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     @Test
     @PortraitLandscape
     public void testDragCustomShortcut() throws Throwable {
+        // TODO(b/322820039): Enable test for tablets - the picker UI has changed and test needs to
+        //  be updated to look for appropriate UI elements.
+        Assume.assumeFalse(mLauncher.isTablet());
         new FavoriteItemsTransaction(mTargetContext).commitAndLoadHome(mLauncher);
 
         mLauncher.getWorkspace().openAllWidgets()