From 4f5b60169d7e8aa04596ce485e95a60ba7a23498 Mon Sep 17 00:00:00 2001 From: Jakob Schneider Date: Wed, 7 Feb 2024 17:29:25 +0000 Subject: [PATCH] Version-2: Prioritize the session-provided icon & label for archived apps during unarchival in the iconCache. * Also ensures that apps are sorted based on their actual name, so that they don't jump around when "Pending.." switches to "Downloading.." * In case of faillure during unarchival, icons shown are reverted to that of PM supplied ones. New UI: http://recall/-/gMbThhDGagWFqnJTbQCqSz/fPuzxUuU7cGXCNdygMkXAB Test: atest CacheDataUpdatedTaskTest.java and locally verified. Bug: 319495216 Flag: ACONFIG com.android.launcher3.enable_support_for_archiving TRUNKFOOD Change-Id: I6410482706af900e273fdc6f7cf0b0692442364c --- src/com/android/launcher3/LauncherModel.java | 28 ++++++- .../launcher3/allapps/AppInfoComparator.java | 14 +++- .../android/launcher3/icons/IconCache.java | 57 +++++++++++++- .../launcher3/model/data/ItemInfo.java | 7 ++ .../model/CacheDataUpdatedTaskTest.java | 78 ++++++++++++++++++- 5 files changed, 173 insertions(+), 11 deletions(-) diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index d124746be7..99fca62ac1 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -441,17 +441,35 @@ public class LauncherModel implements InstallSessionTracker.Callback { @Override public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) { + IconCache iconCache = app.getIconCache(); final IntSet removedIds = new IntSet(); + HashSet archivedItemsToCacheRefresh = new HashSet<>(); + HashSet archivedPackagesToCacheRefresh = new HashSet<>(); synchronized (dataModel) { for (ItemInfo info : dataModel.itemsIdMap) { if (info instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) info).hasPromiseIconUi() && user.equals(info.user) - && info.getIntent() != null - && TextUtils.equals(packageName, info.getIntent().getPackage())) { - removedIds.add(info.id); + && info.getIntent() != null) { + if (TextUtils.equals(packageName, info.getIntent().getPackage())) { + removedIds.add(info.id); + } + if (((WorkspaceItemInfo) info).isArchived()) { + WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info; + // Remove package cache icon for archived app in case of a session + // failure. + mApp.getIconCache().removeIconsForPkg(packageName, user); + // Refresh icons on the workspace for archived apps. + iconCache.getTitleAndIcon(workspaceItem, + workspaceItem.usingLowResIcon()); + archivedPackagesToCacheRefresh.add(packageName); + archivedItemsToCacheRefresh.add(workspaceItem); + } } } + if (!archivedPackagesToCacheRefresh.isEmpty()) { + apps.updateIconsAndLabels(archivedPackagesToCacheRefresh, user); + } } if (!removedIds.isEmpty()) { @@ -459,6 +477,10 @@ public class LauncherModel implements InstallSessionTracker.Callback { ItemInfoMatcher.ofItemIds(removedIds), "removed because install session failed"); } + if (!archivedItemsToCacheRefresh.isEmpty()) { + bindUpdatedWorkspaceItems(archivedItemsToCacheRefresh.stream().toList()); + bindApplicationsIfNeeded(); + } } }); } diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java index 311a40ef64..a0867dbaf1 100644 --- a/src/com/android/launcher3/allapps/AppInfoComparator.java +++ b/src/com/android/launcher3/allapps/AppInfoComparator.java @@ -43,9 +43,7 @@ public class AppInfoComparator implements Comparator { @Override public int compare(AppInfo a, AppInfo b) { // Order by the title in the current locale - int result = mLabelComparator.compare( - a.title == null ? "" : a.title.toString(), - b.title == null ? "" : b.title.toString()); + int result = mLabelComparator.compare(getSortingTitle(a), getSortingTitle(b)); if (result != 0) { return result; } @@ -64,4 +62,14 @@ public class AppInfoComparator implements Comparator { return aUserSerial.compareTo(bUserSerial); } } + + private String getSortingTitle(AppInfo info) { + if (info.appTitle != null) { + return info.appTitle.toString(); + } + if (info.title != null) { + return info.title.toString(); + } + return ""; + } } diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 2f7f51e6f7..d8fa90ae82 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -219,7 +219,19 @@ public class IconCache extends BaseIconCache { CacheEntry entry = cacheLocked(application.componentName, application.user, () -> null, mLauncherActivityInfoCachingLogic, false, application.usingLowResIcon()); - if (entry.bitmap != null && !isDefaultIcon(entry.bitmap, application.user)) { + if (entry.bitmap == null || isDefaultIcon(entry.bitmap, application.user)) { + return; + } + + boolean preferPackageIcon = application.isArchived(); + if (preferPackageIcon) { + String packageName = application.getTargetPackage(); + CacheEntry packageEntry = + cacheLocked(new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + application.user, () -> null, mLauncherActivityInfoCachingLogic, + false, application.usingLowResIcon()); + applyPackageEntry(packageEntry, application, entry); + } else { applyCacheEntry(entry, application); } } @@ -227,10 +239,14 @@ public class IconCache extends BaseIconCache { /** * Fill in {@param info} with the icon and label for {@param activityInfo} */ + @SuppressWarnings("NewApi") public synchronized void getTitleAndIcon(ItemInfoWithIcon info, LauncherActivityInfo activityInfo, boolean useLowResIcon) { + boolean isAppArchived = Utilities.enableSupportForArchiving() && activityInfo != null + && activityInfo.getActivityInfo().isArchived; // If we already have activity info, no need to use package icon - getTitleAndIcon(info, () -> activityInfo, false, useLowResIcon); + getTitleAndIcon(info, () -> activityInfo, isAppArchived, useLowResIcon, + isAppArchived); } /** @@ -309,7 +325,7 @@ public class IconCache extends BaseIconCache { } else { Intent intent = info.getIntent(); getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user), - true, useLowResIcon); + true, useLowResIcon, info.isArchived()); } } @@ -333,6 +349,28 @@ public class IconCache extends BaseIconCache { applyCacheEntry(entry, infoInOut); } + /** + * Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info} + */ + public synchronized void getTitleAndIcon( + @NonNull ItemInfoWithIcon infoInOut, + @NonNull Supplier activityInfoProvider, + boolean usePkgIcon, boolean useLowResIcon, boolean preferPackageEntry) { + CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), infoInOut.user, + activityInfoProvider, mLauncherActivityInfoCachingLogic, usePkgIcon, + useLowResIcon); + if (preferPackageEntry) { + String packageName = infoInOut.getTargetPackage(); + CacheEntry packageEntry = cacheLocked( + new ComponentName(packageName, packageName + EMPTY_CLASS_NAME), + infoInOut.user, activityInfoProvider, mLauncherActivityInfoCachingLogic, + usePkgIcon, useLowResIcon); + applyPackageEntry(packageEntry, infoInOut, entry); + } else { + applyCacheEntry(entry, infoInOut); + } + } + /** * Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles. * @@ -551,6 +589,19 @@ public class IconCache extends BaseIconCache { } } + protected void applyPackageEntry(@NonNull final CacheEntry packageEntry, + @NonNull final ItemInfoWithIcon info, @NonNull final CacheEntry fallbackEntry) { + info.title = Utilities.trim(packageEntry.title); + info.appTitle = Utilities.trim(fallbackEntry.title); + info.contentDescription = packageEntry.contentDescription; + info.bitmap = packageEntry.bitmap; + if (packageEntry.bitmap == null) { + // TODO: entry.bitmap can never be null, so this should not happen at all. + Log.wtf(TAG, "Cannot find bitmap from the cache, default icon was loaded."); + info.bitmap = getDefaultIcon(info.user); + } + } + public Drawable getFullResIcon(LauncherActivityInfo info) { return mIconProvider.getIcon(info, mIconDpi); } diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java index 86393a0012..55849c2149 100644 --- a/src/com/android/launcher3/model/data/ItemInfo.java +++ b/src/com/android/launcher3/model/data/ItemInfo.java @@ -159,6 +159,13 @@ public class ItemInfo { @Nullable public CharSequence title; + /** + * Optionally set: The appTitle might e.g. be different if {@code title} is used to + * display progress (e.g. Downloading..). + */ + @Nullable + public CharSequence appTitle; + /** * Content description of the item. */ diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java index f7710522bb..6c35f68391 100644 --- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java +++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java @@ -2,21 +2,33 @@ package com.android.launcher3.model; import static android.os.Process.myUserHandle; +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import static com.android.launcher3.Flags.FLAG_ENABLE_SUPPORT_FOR_ARCHIVING; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_ARCHIVED; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY2; import static com.android.launcher3.util.LauncherModelHelper.TEST_ACTIVITY3; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; +import static com.android.launcher3.util.TestUtil.DUMMY_CLASS_NAME; +import static com.android.launcher3.util.TestUtil.DUMMY_PACKAGE; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Context; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.uiautomator.UiDevice; import com.android.launcher3.LauncherAppState; import com.android.launcher3.icons.BitmapInfo; @@ -26,12 +38,15 @@ import com.android.launcher3.util.IntSet; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.LauncherModelHelper; import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.util.TestUtil; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -43,9 +58,18 @@ import java.util.List; @RunWith(AndroidJUnit4.class) public class CacheDataUpdatedTaskTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String PENDING_APP_1 = TEST_PACKAGE + ".pending1"; private static final String PENDING_APP_2 = TEST_PACKAGE + ".pending2"; + private static final String ARCHIVED_PACKAGE = DUMMY_PACKAGE; + private static final String ARCHIVED_CLASS_NAME = DUMMY_CLASS_NAME; + private static final String ARCHIVED_TITLE = "Aardwolf"; + + private LauncherModelHelper mModelHelper; private Context mContext; @@ -57,6 +81,7 @@ public class CacheDataUpdatedTaskTest { mContext = mModelHelper.sandboxContext; mSession1 = mModelHelper.createInstallerSession(PENDING_APP_1); mModelHelper.createInstallerSession(PENDING_APP_2); + TestUtil.installDummyApp(); LauncherLayoutBuilder builder = new LauncherLayoutBuilder() .atHotseat(1).putFolder("MyFolder") @@ -73,14 +98,22 @@ public class CacheDataUpdatedTaskTest { .addApp(PENDING_APP_2, TEST_ACTIVITY) // 8 .addApp(PENDING_APP_2, TEST_ACTIVITY2) // 9 .addApp(PENDING_APP_2, TEST_ACTIVITY3) // 10 + + // Dummy Test Package + .addApp(ARCHIVED_PACKAGE, ARCHIVED_CLASS_NAME) // 11 .build(); mModelHelper.setupDefaultLayoutProvider(builder); mModelHelper.loadModelSync(); - assertEquals(10, mModelHelper.getBgDataModel().itemsIdMap.size()); + assertEquals(11, mModelHelper.getBgDataModel().itemsIdMap.size()); + + UiDevice device = UiDevice.getInstance(getInstrumentation()); + assertThat(device.executeShellCommand(String.format("pm archive %s", ARCHIVED_PACKAGE))) + .isEqualTo("Success\n"); } @After - public void tearDown() { + public void tearDown() throws IOException { + TestUtil.uninstallDummyApp(); mModelHelper.destroy(); } @@ -138,6 +171,47 @@ public class CacheDataUpdatedTaskTest { }); } + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_SUPPORT_FOR_ARCHIVING) + public void testSessionUpdate_archivedApps_sessionInfoPrioritized() { + // Run on model executor so that no other task runs in the middle. + runOnExecutorSync(MODEL_EXECUTOR, () -> { + // Clear all icons from apps list so that its easy to check what was updated + allItems().forEach(wi -> wi.bitmap = BitmapInfo.LOW_RES_INFO); + int mSession2 = mModelHelper.createInstallerSession(ARCHIVED_PACKAGE); + mModelHelper.getModel().enqueueModelUpdateTask( + newTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, ARCHIVED_PACKAGE)); + List pendingArchivedAppIds = List.of(11); + // Mark the app items as archived. + allItems().forEach(wi -> { + if (pendingArchivedAppIds.contains(wi.id)) { + wi.runtimeStatusFlags |= FLAG_ARCHIVED; + } + }); + // Before cache is updated with sessionInfo, confirm the title. + for (WorkspaceItemInfo info : allItems()) { + if (pendingArchivedAppIds.contains(info.id)) { + assertEquals(info.title, ARCHIVED_TITLE); + } + } + + // Update the cache with session details. + LauncherAppState.getInstance(mContext).getIconCache().updateSessionCache( + new PackageUserKey(ARCHIVED_PACKAGE, myUserHandle()), + mContext.getPackageManager().getPackageInstaller().getSessionInfo(mSession2)); + + // Trigger a refresh for workspace itemInfo objects. + mModelHelper.getModel().enqueueModelUpdateTask( + newTask(CacheDataUpdatedTask.OP_SESSION_UPDATE, ARCHIVED_PACKAGE)); + // Verify the new title from session is applied to the iconInfo. + for (WorkspaceItemInfo info : allItems()) { + if (pendingArchivedAppIds.contains(info.id)) { + assertEquals(info.title, ARCHIVED_PACKAGE); + } + } + }); + } + private void verifyUpdate(int... idsUpdated) { IntSet updates = IntSet.wrap(idsUpdated); for (WorkspaceItemInfo info : allItems()) {