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()