diff --git a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt index c121340e5e..c673bb3adb 100644 --- a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt +++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt @@ -110,8 +110,6 @@ object CacheableShortcutCachingLogic : CachingLogic { info?.let { max(info.shortcutInfo.lastChangedTimestamp, packageInfo.lastUpdateTime) } ?: packageInfo.lastUpdateTime - override fun addToMemCache() = false - override fun getApplicationInfo(info: CacheableShortcutInfo) = info.appInfo.getInfo() override fun loadIcon(context: Context, cache: BaseIconCache, info: CacheableShortcutInfo) = diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 5396597ab1..ffed1e80d0 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -110,8 +110,7 @@ public class IconCache extends BaseIconCache { IconProvider iconProvider) { super(context, dbFileName, MODEL_EXECUTOR.getLooper(), idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */, iconProvider); - mComponentWithLabelCachingLogic = new CachedObjectCachingLogic( - context, false /* loadIcons */, false /* addToMemCache */); + mComponentWithLabelCachingLogic = new CachedObjectCachingLogic(context); mLauncherActivityInfoCachingLogic = LauncherActivityCachingLogic.INSTANCE; mLauncherApps = mContext.getSystemService(LauncherApps.class); mUserManager = UserCache.INSTANCE.get(mContext); @@ -149,8 +148,7 @@ public class IconCache extends BaseIconCache { PackageManager.GET_UNINSTALLED_PACKAGES); long userSerial = mUserManager.getSerialNumberForUser(user); for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) { - addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, info, userSerial, - false /*replace existing*/); + addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, info, userSerial); } } catch (NameNotFoundException e) { Log.d(TAG, "Package not found", e); @@ -205,7 +203,7 @@ public class IconCache extends BaseIconCache { CancellableTask request = new CancellableTask<>( task, MAIN_EXECUTOR, caller::reapplyItemInfo, endRunnable); - Utilities.postAsyncCallback(mWorkerHandler, request); + Utilities.postAsyncCallback(workerHandler, request); return request; } @@ -257,6 +255,7 @@ public class IconCache extends BaseIconCache { /** * Fill in {@code info} with the icon and label for {@code si}. If the icon is not * available, and fallback check returns true, it keeps the old icon. + * Shortcut entries are not kept in memory since they are not frequently used */ public void getShortcutIcon(T info, CacheableShortcutInfo si, @NonNull Predicate fallbackIconCheck) { @@ -266,7 +265,7 @@ public class IconCache extends BaseIconCache { user, () -> si, CacheableShortcutCachingLogic.INSTANCE, - LookupFlag.DEFAULT).bitmap; + LookupFlag.SKIP_ADD_TO_MEM_CACHE).bitmap; if (bitmapInfo.isNullOrLowRes()) { bitmapInfo = getDefaultIcon(user); } @@ -337,7 +336,8 @@ public class IconCache extends BaseIconCache { */ public synchronized String getTitleNoCache(ComponentWithLabel info) { CacheEntry entry = cacheLocked(info.getComponent(), info.getUser(), () -> info, - mComponentWithLabelCachingLogic, LookupFlag.USE_LOW_RES); + mComponentWithLabelCachingLogic, + LookupFlag.USE_LOW_RES | LookupFlag.SKIP_ADD_TO_MEM_CACHE); return Utilities.trim(entry.title); } diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java index 495d583b09..1f0e750568 100644 --- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java @@ -15,21 +15,39 @@ */ package com.android.launcher3.icons; +import static android.os.Process.myUserHandle; + import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE; +import static com.android.launcher3.icons.IconCacheUpdateHandlerTestKt.waitForUpdateHandlerToFinish; +import static com.android.launcher3.model.data.AppInfo.makeLaunchIntent; 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_PACKAGE; +import static com.android.launcher3.util.TestUtil.runOnExecutorSync; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.pm.LauncherActivityInfo; +import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo.Builder; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.drawable.Icon; import android.os.PersistableBundle; +import android.os.UserHandle; import android.text.TextUtils; import androidx.annotation.Nullable; @@ -37,15 +55,30 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.icons.cache.CachingLogic; +import com.android.launcher3.icons.cache.IconCacheUpdateHandler; +import com.android.launcher3.icons.cache.LauncherActivityCachingLogic; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.PackageItemInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.settings.SettingsActivity; +import com.android.launcher3.shortcuts.ShortcutKey; +import com.android.launcher3.util.ApplicationInfoWrapper; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.util.RoboApiWrapper; + +import com.google.common.truth.Truth; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + @SmallTest @RunWith(AndroidJUnit4.class) public class IconCacheTest { @@ -112,6 +145,102 @@ public class IconCacheTest { assertEquals(((PackageItemInfo) item).packageName, otherPackage); } + @Test + public void launcherActivityInfo_cached_in_memory() { + RoboApiWrapper.INSTANCE.initialize(); + ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + UserHandle user = myUserHandle(); + ComponentKey cacheKey = new ComponentKey(cn, user); + + LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) + .resolveActivity(makeLaunchIntent(cn), user); + assertNotNull(lai); + + WorkspaceItemInfo info = new WorkspaceItemInfo(); + info.intent = makeLaunchIntent(cn); + runOnExecutorSync(MODEL_EXECUTOR, + () -> mIconCache.getTitleAndIcon(info, lai, false)); + assertNotNull(info.bitmap); + assertFalse(info.bitmap.isLowRes()); + + // Verify that icon is in memory cache + runOnExecutorSync(MODEL_EXECUTOR, + () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey))); + + // Schedule async update and wait for it to complete + Set updates = + executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE); + + // Verify that the icon was not updated and is still in memory cache + Truth.assertThat(updates).isEmpty(); + runOnExecutorSync(MODEL_EXECUTOR, + () -> assertNotNull(mIconCache.getInMemoryEntryLocked(cacheKey))); + } + + @Test + public void shortcutInfo_not_cached_in_memory() { + CacheableShortcutInfo si = mockShortcutInfo(0); + ShortcutKey cacheKey = ShortcutKey.fromInfo(si.getShortcutInfo()); + + WorkspaceItemInfo info = new WorkspaceItemInfo(); + runOnExecutorSync(MODEL_EXECUTOR, () -> mIconCache.getShortcutIcon(info, si)); + assertNotNull(info.bitmap); + assertFalse(info.bitmap.isLowRes()); + + // Verify that icon is in memory cache + runOnExecutorSync(MODEL_EXECUTOR, + () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); + + Set updates = + executeIconUpdate(si, CacheableShortcutCachingLogic.INSTANCE); + // Verify that the icon was not updated and is still in memory cache + Truth.assertThat(updates).isEmpty(); + runOnExecutorSync(MODEL_EXECUTOR, + () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); + + // Now update the shortcut with a newer version + updates = executeIconUpdate( + mockShortcutInfo(System.currentTimeMillis() + 2000), + CacheableShortcutCachingLogic.INSTANCE); + + // Verify that icon was updated but it is still not in mem-cache + Truth.assertThat(updates).containsExactly( + new PackageUserKey(cacheKey.getPackageName(), cacheKey.user)); + runOnExecutorSync(MODEL_EXECUTOR, + () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); + } + + /** + * Executes the icon update for the provided entry and returns the updated packages + */ + private Set executeIconUpdate(T object, CachingLogic cachingLogic) { + HashSet updates = new HashSet<>(); + + runOnExecutorSync(MODEL_EXECUTOR, () -> { + IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler(); + updateHandler.updateIcons( + Collections.singletonList(object), + cachingLogic, + (a, b) -> a.forEach(p -> updates.add(new PackageUserKey(p, b)))); + updateHandler.finish(); + }); + waitForUpdateHandlerToFinish(mIconCache); + return updates; + } + + private CacheableShortcutInfo mockShortcutInfo(long updateTime) { + ShortcutInfo info = new ShortcutInfo.Builder( + getInstrumentation().getContext(), "test-shortcut") + .setIntent(new Intent(Intent.ACTION_VIEW)) + .setShortLabel("Test") + .setIcon(Icon.createWithBitmap(Bitmap.createBitmap(200, 200, Config.ARGB_8888))) + .build(); + ShortcutInfo spied = spy(info); + doReturn(updateTime).when(spied).getLastChangedTimestamp(); + return new CacheableShortcutInfo(spied, + new ApplicationInfoWrapper(getInstrumentation().getContext().getApplicationInfo())); + } + private ItemInfoWithIcon getBadgingInfo(Context context, @Nullable ComponentName cn, @Nullable String badgeOverride) throws Exception { Builder builder = new Builder(context, "test-shortcut") diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt index e27926f0ff..b54636c756 100644 --- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt @@ -25,6 +25,8 @@ import androidx.test.filters.SmallTest import com.android.launcher3.icons.cache.BaseIconCache import com.android.launcher3.icons.cache.CachingLogic import com.android.launcher3.icons.cache.IconCacheUpdateHandler +import com.android.launcher3.util.RoboApiWrapper +import java.util.concurrent.FutureTask import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -51,7 +53,7 @@ class IconCacheUpdateHandlerTest { System.currentTimeMillis(), 1, 1.0.toLong(), - "stateOfConfusion" + "stateOfConfusion", ) @Before @@ -81,17 +83,32 @@ class IconCacheUpdateHandlerTest { componentMap, ignorePackages, user, - cachingLogic + cachingLogic, ) assert(result == null) } } +/** Utility method to wait for the icon update handler to finish */ +fun IconCache.waitForUpdateHandlerToFinish() { + var cacheUpdateInProgress = true + while (cacheUpdateInProgress) { + val cacheCheck = FutureTask { + // Check for pending message on the worker thread itself as some task may be + // running currently + workerHandler.hasMessages(0, iconUpdateToken) + } + workerHandler.postDelayed(cacheCheck, 10) + RoboApiWrapper.waitForLooperSync(workerHandler.looper) + cacheUpdateInProgress = cacheCheck.get() + } +} + data class IconCacheRowData( val component: String, val lastUpdated: Long, val version: Int, val row: Long, - val systemState: String + val systemState: String, ) diff --git a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt index 371bac2cc4..4ca47e3eb6 100644 --- a/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/model/FolderIconLoadTest.kt @@ -19,16 +19,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.launcher3.LauncherAppState import com.android.launcher3.icons.BitmapInfo +import com.android.launcher3.icons.waitForUpdateHandlerToFinish import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.util.Executors import com.android.launcher3.util.LauncherLayoutBuilder import com.android.launcher3.util.LauncherModelHelper import com.android.launcher3.util.LauncherModelHelper.* -import com.android.launcher3.util.RoboApiWrapper import com.android.launcher3.util.TestUtil import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import java.util.concurrent.CountDownLatch import org.junit.After import org.junit.Before import org.junit.Rule @@ -147,14 +146,9 @@ class FolderIconLoadTest { // The first load initializes the DB, load again so that icons are now used from the DB // Wait for the icon cache to be updated and then reload val app = LauncherAppState.getInstance(modelHelper.sandboxContext) - val cache = app.iconCache - while (cache.isIconUpdateInProgress) { - val wait = CountDownLatch(1) - Executors.MODEL_EXECUTOR.handler.postDelayed({ wait.countDown() }, 10) - RoboApiWrapper.waitForLooperSync(Executors.MODEL_EXECUTOR.handler.looper) - wait.await() - } - TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { cache.clearMemoryCache() } + app.iconCache.waitForUpdateHandlerToFinish() + + TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { app.iconCache.clearMemoryCache() } // Reload again with correct icon state app.model.forceReload() modelHelper.loadModelSync()