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
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index d124746..99fca62 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -441,17 +441,35 @@
@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<WorkspaceItemInfo> archivedItemsToCacheRefresh = new HashSet<>();
+ HashSet<String> 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 @@
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 311a40e..a0867db 100644
--- a/src/com/android/launcher3/allapps/AppInfoComparator.java
+++ b/src/com/android/launcher3/allapps/AppInfoComparator.java
@@ -43,9 +43,7 @@
@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 @@
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 2f7f51e..d8fa90a 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -219,7 +219,19 @@
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 @@
/**
* 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 @@
} else {
Intent intent = info.getIntent();
getTitleAndIcon(info, () -> mLauncherApps.resolveActivity(intent, info.user),
- true, useLowResIcon);
+ true, useLowResIcon, info.isArchived());
}
}
@@ -334,6 +350,28 @@
}
/**
+ * Fill in {@param mWorkspaceItemInfo} with the icon and label for {@param info}
+ */
+ public synchronized void getTitleAndIcon(
+ @NonNull ItemInfoWithIcon infoInOut,
+ @NonNull Supplier<LauncherActivityInfo> 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.
*
* @param iconRequestInfos List of IconRequestInfos representing titles and icons to query.
@@ -551,6 +589,19 @@
}
}
+ 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 86393a0..55849c2 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -160,6 +160,13 @@
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.
*/
@Nullable
diff --git a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
index f771052..6c35f68 100644
--- a/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
+++ b/tests/src/com/android/launcher3/model/CacheDataUpdatedTaskTest.java
@@ -2,21 +2,33 @@
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.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 @@
@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 @@
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 @@
.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 @@
});
}
+ @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<Integer> 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()) {