diff --git a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt index 04696364e0..f542b8c0b3 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt +++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt @@ -23,6 +23,7 @@ import android.content.IIntentReceiver import android.content.IIntentSender import android.content.Intent import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps import android.content.pm.ShortcutInfo @@ -76,7 +77,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { UserManager.USER_TYPE_PROFILE_PRIVATE -> UserIconInfo.TYPE_PRIVATE else -> UserIconInfo.TYPE_MAIN }, - userSerialNumber.toLong() + userSerialNumber.toLong(), ) } } @@ -110,7 +111,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { ) .toBundle() requireActivityResult = false - } + }, ) else super.getAppMarketActivityIntent(packageName, user) @@ -131,7 +132,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { ) .toBundle() requireActivityResult = false - } + }, ) else null @@ -160,7 +161,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { allowlistToken: IBinder?, finishedReceiver: IIntentReceiver?, requiredPermission: String?, - options: Bundle? + options: Bundle?, ) { if (code != -1) { Executors.MAIN_EXECUTOR.execute { @@ -168,9 +169,9 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { context, context.getString( R.string.set_default_home_app, - context.getString(R.string.derived_app_name) + context.getString(R.string.derived_app_name), ), - Toast.LENGTH_LONG + Toast.LENGTH_LONG, ) .show() } @@ -183,4 +184,7 @@ open class SystemApiWrapper(context: Context?) : ApiWrapper(context) { context.startActivity(ProxyActivityStarter.getLaunchIntent(context, params)) } } + + override fun getApplicationInfoHash(appInfo: ApplicationInfo): String = + (appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode } diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 8121e53542..76dc7700e7 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -220,7 +220,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private CancellableTask mIconLoadRequest; - private boolean mEnableIconUpdateAnimation = false; + private boolean mHighResUpdateInProgress = false; public BubbleTextView(Context context) { this(context, null, 0); @@ -1195,10 +1195,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } } - protected boolean iconUpdateAnimationEnabled() { - return mEnableIconUpdateAnimation; - } - protected void applyCompoundDrawables(Drawable icon) { if (icon == null) { // Icon can be null when we use the BubbleTextView for text only. @@ -1216,7 +1212,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, // If the current icon is a placeholder color, animate its update. if (mIcon != null && mIcon instanceof PlaceHolderIconDrawable - && iconUpdateAnimationEnabled()) { + && mHighResUpdateInProgress) { ((PlaceHolderIconDrawable) mIcon).animateIconUpdate(icon); } @@ -1238,7 +1234,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, if (getTag() == info) { mIconLoadRequest = null; mDisableRelayout = true; - mEnableIconUpdateAnimation = true; + mHighResUpdateInProgress = true; // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. info.bitmap.icon.prepareToDraw(); @@ -1253,7 +1249,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } mDisableRelayout = false; - mEnableIconUpdateAnimation = false; + mHighResUpdateInProgress = false; } } @@ -1265,7 +1261,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, mIconLoadRequest.cancel(); mIconLoadRequest = null; } - if (getTag() instanceof ItemInfoWithIcon) { + if (getTag() instanceof ItemInfoWithIcon && !mHighResUpdateInProgress) { ItemInfoWithIcon info = (ItemInfoWithIcon) getTag(); if (info.usingLowResIcon()) { mIconLoadRequest = LauncherAppState.getInstance(getContext()).getIconCache() diff --git a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt index c673bb3adb..f8a555258a 100644 --- a/src/com/android/launcher3/icons/CacheableShortcutInfo.kt +++ b/src/com/android/launcher3/icons/CacheableShortcutInfo.kt @@ -20,11 +20,9 @@ import android.content.ComponentName import android.content.Context import android.content.pm.LauncherActivityInfo import android.content.pm.LauncherApps -import android.content.pm.PackageInfo import android.content.pm.ShortcutInfo import android.graphics.drawable.Drawable import android.os.UserHandle -import android.text.TextUtils import android.util.Log import com.android.launcher3.BuildConfig import com.android.launcher3.LauncherAppState @@ -35,7 +33,6 @@ import com.android.launcher3.shortcuts.ShortcutKey import com.android.launcher3.util.ApplicationInfoWrapper import com.android.launcher3.util.PackageUserKey import com.android.launcher3.util.Themes -import kotlin.math.max /** Wrapper over ShortcutInfo to provide extra information related to ShortcutInfo */ class CacheableShortcutInfo(val shortcutInfo: ShortcutInfo, val appInfo: ApplicationInfoWrapper) { @@ -103,13 +100,6 @@ object CacheableShortcutCachingLogic : CachingLogic { override fun getLabel(info: CacheableShortcutInfo): CharSequence? = info.shortcutInfo.shortLabel - override fun getDescription(info: CacheableShortcutInfo, fallback: CharSequence): CharSequence = - info.shortcutInfo.longLabel.let { if (TextUtils.isEmpty(it)) fallback else it!! } - - override fun getLastUpdatedTime(info: CacheableShortcutInfo?, packageInfo: PackageInfo) = - info?.let { max(info.shortcutInfo.lastChangedTimestamp, packageInfo.lastUpdateTime) } - ?: packageInfo.lastUpdateTime - override fun getApplicationInfo(info: CacheableShortcutInfo) = info.appInfo.getInfo() override fun loadIcon(context: Context, cache: BaseIconCache, info: CacheableShortcutInfo) = @@ -126,4 +116,12 @@ object CacheableShortcutCachingLogic : CachingLogic { ) } ?: BitmapInfo.LOW_RES_INFO } + + override fun getFreshnessIdentifier( + item: CacheableShortcutInfo, + provider: IconProvider, + ): String? = + item.shortcutInfo.lastChangedTimestamp.toString() + + "-" + + provider.getStateForApp(getApplicationInfo(item)) } diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index ffed1e80d0..b6fe66a1f2 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -29,10 +29,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; -import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ShortcutInfo; import android.database.Cursor; import android.database.sqlite.SQLiteException; @@ -66,6 +63,7 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.pm.InstallSessionHelper; import com.android.launcher3.pm.UserCache; import com.android.launcher3.util.CancellableTask; +import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.WidgetSections; @@ -143,15 +141,9 @@ public class IconCache extends BaseIconCache { public synchronized void updateIconsForPkg(@NonNull final String packageName, @NonNull final UserHandle user) { removeIconsForPkg(packageName, user); - try { - PackageInfo info = mPackageManager.getPackageInfo(packageName, - PackageManager.GET_UNINSTALLED_PACKAGES); - long userSerial = mUserManager.getSerialNumberForUser(user); - for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) { - addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, info, userSerial); - } - } catch (NameNotFoundException e) { - Log.d(TAG, "Package not found", e); + long userSerial = mUserManager.getSerialNumberForUser(user); + for (LauncherActivityInfo app : mLauncherApps.getActivityList(packageName, user)) { + addIconToDBAndMemCache(app, mLauncherActivityInfoCachingLogic, userSerial); } } @@ -602,6 +594,11 @@ public class IconCache extends BaseIconCache { info.getAppLabel()); } + @VisibleForTesting + synchronized boolean isItemInDb(ComponentKey cacheKey) { + return getEntryFromDBLocked(cacheKey, new CacheEntry(), false); + } + /** * Interface for receiving itemInfo with high-res icon. */ diff --git a/src/com/android/launcher3/icons/LauncherIconProvider.java b/src/com/android/launcher3/icons/LauncherIconProvider.java index c4d5f2b5b5..78a31285f9 100644 --- a/src/com/android/launcher3/icons/LauncherIconProvider.java +++ b/src/com/android/launcher3/icons/LauncherIconProvider.java @@ -16,14 +16,18 @@ package com.android.launcher3.icons; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; +import androidx.annotation.NonNull; + import com.android.launcher3.R; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.ApiWrapper; import com.android.launcher3.util.Themes; import org.xmlpull.v1.XmlPullParser; @@ -70,6 +74,11 @@ public class LauncherIconProvider extends IconProvider { return super.getSystemIconState() + (mSupportsIconTheme ? ",with-theme" : ",no-theme"); } + @Override + protected String getApplicationInfoHash(@NonNull ApplicationInfo appInfo) { + return ApiWrapper.INSTANCE.get(mContext).getApplicationInfoHash(appInfo); + } + private Map getThemedIconMap() { if (mThemedIconMap != null) { return mThemedIconMap; diff --git a/src/com/android/launcher3/util/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java index 095518c8ea..21f91acdae 100644 --- a/src/com/android/launcher3/util/ApiWrapper.java +++ b/src/com/android/launcher3/util/ApiWrapper.java @@ -24,6 +24,7 @@ import android.app.Person; import android.app.role.RoleManager; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.LauncherActivityInfo; import android.content.pm.ShortcutInfo; import android.graphics.drawable.ColorDrawable; @@ -32,6 +33,7 @@ import android.os.UserHandle; import android.os.UserManager; import android.util.ArrayMap; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.BuildConfig; @@ -156,6 +158,14 @@ public class ApiWrapper implements ResourceBasedOverride, SafeCloseable { } } + /** + * Returns a hash to uniquely identify a particular version of appInfo + */ + public String getApplicationInfoHash(@NonNull ApplicationInfo appInfo) { + // The hashString in source dir changes with every install + return appInfo.sourceDir; + } + @Override public void close() { } diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java index 1f0e750568..519108d3ec 100644 --- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java +++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheTest.java @@ -24,6 +24,7 @@ import static com.android.launcher3.icons.IconCacheUpdateHandlerTestKt.waitForUp 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_ACTIVITY2; import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; @@ -210,6 +211,70 @@ public class IconCacheTest { () -> assertNull(mIconCache.getInMemoryEntryLocked(cacheKey))); } + @Test + public void item_kept_in_db_if_nothing_changes() { + RoboApiWrapper.INSTANCE.initialize(); + ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + UserHandle user = myUserHandle(); + + LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) + .resolveActivity(makeLaunchIntent(cn), user); + assertNotNull(lai); + + // Since this is a new update, there should not be any update + Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); + assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); + + // Another update should not cause any changes + Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); + assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); + } + + @Test + public void item_updated_in_db_if_appInfo_changes() { + RoboApiWrapper.INSTANCE.initialize(); + ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + UserHandle user = myUserHandle(); + + LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) + .resolveActivity(makeLaunchIntent(cn), user); + assertNotNull(lai); + + // Since this is a new update, there should not be any update + Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); + assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); + + // Another update should trigger an update + lai.getApplicationInfo().sourceDir = "some-random-source-dir"; + Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)) + .containsExactly(new PackageUserKey(TEST_PACKAGE, user)); + assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); + } + + @Test + public void item_removed_in_db_if_item_removed() { + RoboApiWrapper.INSTANCE.initialize(); + ComponentName cn = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY); + UserHandle user = myUserHandle(); + + LauncherActivityInfo lai = mContext.getSystemService(LauncherApps.class) + .resolveActivity(makeLaunchIntent(cn), user); + assertNotNull(lai); + + // Since this is a new update, there should not be any update + Truth.assertThat(executeIconUpdate(lai, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); + assertTrue(mIconCache.isItemInDb(new ComponentKey(cn, user))); + + // Another update should trigger an update + ComponentName cn2 = new ComponentName(TEST_PACKAGE, TEST_ACTIVITY2); + LauncherActivityInfo lai2 = mContext.getSystemService(LauncherApps.class) + .resolveActivity(makeLaunchIntent(cn2), user); + + Truth.assertThat(executeIconUpdate(lai2, LauncherActivityCachingLogic.INSTANCE)).isEmpty(); + assertFalse(mIconCache.isItemInDb(new ComponentKey(cn, user))); + assertTrue(mIconCache.isItemInDb(new ComponentKey(cn2, user))); + } + /** * Executes the icon update for the provided entry and returns the updated packages */ diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt index b54636c756..8e54c9451c 100644 --- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt @@ -17,77 +17,109 @@ package com.android.launcher3.icons import android.content.ComponentName -import android.content.pm.PackageInfo -import android.database.Cursor -import android.os.UserHandle +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.database.MatrixCursor +import android.os.Process.myUserHandle +import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 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.BaseIconCache.IconDB +import com.android.launcher3.icons.cache.CachedObject +import com.android.launcher3.icons.cache.CachedObjectCachingLogic import com.android.launcher3.icons.cache.IconCacheUpdateHandler import com.android.launcher3.util.RoboApiWrapper +import com.google.common.truth.Truth.assertThat import java.util.concurrent.FutureTask +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class IconCacheUpdateHandlerTest { - @Mock private lateinit var cursor: Cursor - @Mock private lateinit var user: UserHandle - @Mock private lateinit var cachingLogic: CachingLogic + @Mock private lateinit var iconProvider: IconProvider @Mock private lateinit var baseIconCache: BaseIconCache - private var componentMap: HashMap = hashMapOf() - private var ignorePackages: Set = setOf() - private var packageInfoMap: HashMap = hashMapOf() - - private val dummyRowData = - IconCacheRowData( - "com.android.fake/.FakeActivity", - System.currentTimeMillis(), - 1, - 1.0.toLong(), - "stateOfConfusion", - ) + private var cursor: MatrixCursor? = null + private var cachingLogic = CachedObjectCachingLogic(getApplicationContext()) @Before fun setup() { - MockitoAnnotations.initMocks(this) - // Load in a specific row to the database - doReturn(0).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_COMPONENT) - doReturn(1).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_LAST_UPDATED) - doReturn(2).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_VERSION) - doReturn(3).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_ROWID) - doReturn(4).`when`(cursor).getColumnIndex(BaseIconCache.IconDB.COLUMN_SYSTEM_STATE) - doReturn(dummyRowData.component).`when`(cursor).getString(0) - doReturn(dummyRowData.lastUpdated).`when`(cursor).getLong(1) - doReturn(dummyRowData.version).`when`(cursor).getInt(2) - doReturn(dummyRowData.row).`when`(cursor).getLong(3) - doReturn(dummyRowData.systemState).`when`(cursor).getString(4) + doReturn(iconProvider).whenever(baseIconCache).iconProvider + } + + @After + fun tearDown() { + cursor?.close() } @Test fun `IconCacheUpdateHandler returns null if the component name is malformed`() { - val updateHandlerUnderTest = IconCacheUpdateHandler(packageInfoMap, baseIconCache) + val updateHandlerUnderTest = IconCacheUpdateHandler(baseIconCache) + val cn = ComponentName.unflattenFromString("com.android.fake/.FakeActivity")!! val result = updateHandlerUnderTest.updateOrDeleteIcon( - cursor, - componentMap, - ignorePackages, - user, + createCursor(1, cn.flattenToString() + "#", "freshId-old"), + hashMapOf(cn to TestCachedObject(cn, "freshId")), + setOf(), + myUserHandle(), cachingLogic, ) - - assert(result == null) + assertThat(result).isNull() } + + @Test + fun `IconCacheUpdateHandler returns null if the freshId match`() { + val updateHandlerUnderTest = IconCacheUpdateHandler(baseIconCache) + val cn = ComponentName.unflattenFromString("com.android.fake/.FakeActivity")!! + + val result = + updateHandlerUnderTest.updateOrDeleteIcon( + createCursor(1, cn.flattenToString(), "freshId"), + hashMapOf(cn to TestCachedObject(cn, "freshId")), + setOf(), + myUserHandle(), + cachingLogic, + ) + assertThat(result).isNull() + } + + @Test + fun `IconCacheUpdateHandler returns non-null if the freshId do not match`() { + val updateHandlerUnderTest = IconCacheUpdateHandler(baseIconCache) + val cn = ComponentName.unflattenFromString("com.android.fake/.FakeActivity")!! + val testObj = TestCachedObject(cn, "freshId") + + val result = + updateHandlerUnderTest.updateOrDeleteIcon( + createCursor(1, cn.flattenToString(), "freshId-old"), + hashMapOf(cn to testObj), + setOf(), + myUserHandle(), + cachingLogic, + ) + assertThat(result).isEqualTo(testObj) + } + + private fun createCursor(row: Long, component: String, appState: String) = + MatrixCursor( + arrayOf(IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID) + ) + .apply { addRow(arrayOf(row, component, appState)) } + .apply { + cursor = this + moveToNext() + } } /** Utility method to wait for the icon update handler to finish */ @@ -105,10 +137,16 @@ fun IconCache.waitForUpdateHandlerToFinish() { } } -data class IconCacheRowData( - val component: String, - val lastUpdated: Long, - val version: Int, - val row: Long, - val systemState: String, -) +class TestCachedObject(val cn: ComponentName, val freshnessId: String) : + CachedObject { + + override fun getComponent() = cn + + override fun getUser() = myUserHandle() + + override fun getLabel(pm: PackageManager?): CharSequence? = null + + override fun getApplicationInfo(): ApplicationInfo? = null + + override fun getFreshnessIdentifier(iconProvider: IconProvider): String? = freshnessId +} diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt index ec83b8b120..7484bce091 100644 --- a/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt @@ -38,8 +38,8 @@ class GeneratedPreviewTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() private val providerName = ComponentName( - "com.android.launcher3.tests", - "com.android.launcher3.testcomponent.AppWidgetNoConfig" + getInstrumentation().getContext().getPackageName(), + "com.android.launcher3.testcomponent.AppWidgetNoConfig", ) private val generatedPreviewLayout = getInstrumentation().context.run { @@ -61,7 +61,7 @@ class GeneratedPreviewTest { ActivityContextWrapper( ContextThemeWrapper( context, - com.android.launcher3.R.style.WidgetContainerTheme + com.android.launcher3.R.style.WidgetContainerTheme, ) ) ) @@ -78,7 +78,7 @@ class GeneratedPreviewTest { object : WidgetManagerHelper(context) { override fun loadGeneratedPreview( info: AppWidgetProviderInfo, - widgetCategory: Int + widgetCategory: Int, ) = generatedPreview.takeIf { info === appWidgetProviderInfo &&