From f6efa25a496444249c41e7d9c81ebf0a71c03a46 Mon Sep 17 00:00:00 2001 From: Shamali P Date: Thu, 20 Feb 2025 18:33:25 +0000 Subject: [PATCH] Update widgetsModel to return pickable vs all widgets separately. Earlier wallpaper preview was reading widgets that were eligible for displaying in picker, which means widgets that are marked as hidden in picker wouldn't show up in wallpaper preview. This fix updates widgets model to maintain map of all widgets (instead of just pickable widgets like before), so that the existing `getWidgetsByComponentKey` function used by wallpaper preview can see all widgets. And, updates picker specific methods to use separate functions (suffixed `forPicker` that filter out picker ineligible widgets when read by picker code). Bug: 385695615 Test: WidgetsModelTest, WidgetsPredictionUpdateTaskTest and demo Flag: EXEMPT BUGFIX Change-Id: I59efe38be0ce1f8a956ba4be42fb6e8b48b5d323 --- .../launcher3/WidgetPickerActivity.java | 5 +- .../model/WidgetsPredictionUpdateTask.java | 2 +- .../WidgetsPredicationUpdateTaskTest.java | 35 +++++++++++- .../launcher3/model/BaseLauncherBinder.java | 2 +- .../launcher3/model/ModelTaskController.kt | 2 +- .../android/launcher3/model/WidgetsModel.java | 55 ++++++++++++++++--- .../launcher3/model/WidgetsModelTest.kt | 50 +++++++++++++++-- .../android/launcher3/util/WidgetUtils.java | 20 ++++++- 8 files changed, 151 insertions(+), 20 deletions(-) diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java index 1cf7dda3af..f992913cde 100644 --- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java +++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java @@ -322,13 +322,14 @@ public class WidgetPickerActivity extends BaseActivity { stringCache.loadStrings(this); bindStringCache(stringCache); - bindWidgets(mModel.getWidgetsByPackageItem(), mModel.getDefaultWidgetsFilter()); + bindWidgets(mModel.getWidgetsByPackageItemForPicker(), + mModel.getDefaultWidgetsFilter()); // Open sheet once widgets are available, so that it doesn't interrupt the open // animation. openWidgetsSheet(); if (mUiSurface != null) { mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(), - mUiSurface, mModel.getWidgetsByComponentKey()); + mUiSurface, mModel.getWidgetsByComponentKeyForPicker()); mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); } }); diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java index 40e1c10d1c..9626a61e80 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -79,7 +79,7 @@ public final class WidgetsPredictionUpdateTask implements ModelUpdateTask { // Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for // being in predictions. Map allEligibleWidgets = - dataModel.widgetsModel.getWidgetsByComponentKey() + dataModel.widgetsModel.getWidgetsByComponentKeyForPicker() .entrySet() .stream() .filter(entry -> entry.getValue().widgetInfo != null diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index fa2eb1ee14..d52d0548f9 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -91,6 +91,7 @@ public final class WidgetsPredicationUpdateTaskTest { private AppWidgetProviderInfo mApp4Provider1; private AppWidgetProviderInfo mApp4Provider2; private AppWidgetProviderInfo mApp5Provider1; + private AppWidgetProviderInfo mApp6PinOnlyProvider1; private List allWidgets; private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback(); @@ -117,8 +118,14 @@ public final class WidgetsPredicationUpdateTaskTest { ComponentName.createRelative("app4", ".provider2")); mApp5Provider1 = createAppWidgetProviderInfo( ComponentName.createRelative("app5", "provider1")); + mApp6PinOnlyProvider1 = createAppWidgetProviderInfo( + ComponentName.createRelative("app6", "provider1"), + /*hideFromPicker=*/ true + ); + + allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, - mApp4Provider1, mApp4Provider2, mApp5Provider1); + mApp4Provider1, mApp4Provider2, mApp5Provider1, mApp6PinOnlyProvider1); mLauncherApps = mModelHelper.sandboxContext.spyService(LauncherApps.class); doAnswer(i -> { @@ -270,6 +277,32 @@ public final class WidgetsPredicationUpdateTaskTest { }); } + @Test + public void widgetsRecommendations_excludesWidgetsHiddenForPicker() { + runOnExecutorSync(MODEL_EXECUTOR, () -> { + + // Not installed widget - hence eligible + AppTarget widget1 = new AppTarget(new AppTargetId("app1"), "app1", "provider1", + mUserHandle); + // Provider marked as hidden from picker - hence not eligible + AppTarget widget6 = new AppTarget(new AppTargetId("app6"), "app6", "provider1", + mUserHandle); + + mCallback.mRecommendedWidgets = null; + mModelHelper.getModel().enqueueModelUpdateTask( + newWidgetsPredicationTask(List.of(widget1, widget6))); + runOnExecutorSync(MAIN_EXECUTOR, () -> { }); + + // Only widget 1 (and no widget 6 as its meant to be hidden from picker). + List recommendedWidgets = mCallback.mRecommendedWidgets.items + .stream() + .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) + .collect(Collectors.toList()); + assertThat(recommendedWidgets).hasSize(1); + assertThat(recommendedWidgets.get(0).componentName.getPackageName()).isEqualTo("app1"); + }); + } + private void assertWidgetInfo( LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) { assertThat(actual.provider).isEqualTo(expected.provider); diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java index 003bef3311..3ee8b87db0 100644 --- a/src/com/android/launcher3/model/BaseLauncherBinder.java +++ b/src/com/android/launcher3/model/BaseLauncherBinder.java @@ -167,7 +167,7 @@ public class BaseLauncherBinder { return; } Map> - widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem(); + widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker(); List widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext()) .build(widgetsByPackageItem); Predicate filter = mBgDataModel.widgetsModel.getDefaultWidgetsFilter(); diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt index 40ea17d437..6e3e35ea1b 100644 --- a/src/com/android/launcher3/model/ModelTaskController.kt +++ b/src/com/android/launcher3/model/ModelTaskController.kt @@ -77,7 +77,7 @@ class ModelTaskController( } fun bindUpdatedWidgets(dataModel: BgDataModel) { - val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItem + val widgetsByPackageItem = dataModel.widgetsModel.widgetsByPackageItemForPicker val allWidgets = WidgetsListBaseEntriesBuilder(app.context).build(widgetsByPackageItem) val defaultWidgetsFilter = dataModel.widgetsModel.defaultWidgetsFilter diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java index a17646556b..ab960d8169 100644 --- a/src/com/android/launcher3/model/WidgetsModel.java +++ b/src/com/android/launcher3/model/WidgetsModel.java @@ -70,6 +70,7 @@ public class WidgetsModel { private final Map> mWidgetsByPackageItem = new HashMap<>(); @Nullable private Predicate mDefaultWidgetsFilter = null; @Nullable private Predicate mPredictedWidgetsFilter = null; + @Nullable private WidgetValidityCheckForPicker mWidgetValidityCheckForPicker = null; /** * Returns all widgets keyed by their component key. @@ -87,13 +88,44 @@ public class WidgetsModel { } /** - * Returns widgets grouped by the package item that they should belong to. + * Returns widgets (eligible for display in picker) keyed by their component key. */ - public synchronized Map> getWidgetsByPackageItem() { - if (!WIDGETS_ENABLED) { + public synchronized Map getWidgetsByComponentKeyForPicker() { + if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) { return Collections.emptyMap(); } - return new HashMap<>(mWidgetsByPackageItem); + + return mWidgetsByPackageItem.values().stream() + .flatMap(Collection::stream).distinct() + .filter(widgetItem -> mWidgetValidityCheckForPicker.test(widgetItem)) + .collect(Collectors.toMap( + widget -> new ComponentKey(widget.componentName, widget.user), + Function.identity() + )); + } + + /** + * Returns widgets (displayable in the widget picker) grouped by the package item that + * they should belong to. + */ + public synchronized Map> getWidgetsByPackageItemForPicker() { + if (!WIDGETS_ENABLED || mWidgetValidityCheckForPicker == null) { + return Collections.emptyMap(); + } + + return mWidgetsByPackageItem.entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().stream() + .filter(widgetItem -> + mWidgetValidityCheckForPicker.test(widgetItem)) + .collect(Collectors.toList()) + ) + ) + .entrySet().stream() + .filter(entry -> !entry.getValue().isEmpty()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } /** @@ -181,6 +213,9 @@ public class WidgetsModel { Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size()); } + // Refresh the validity checker with latest app state. + mWidgetValidityCheckForPicker = new WidgetValidityCheckForPicker(app); + // Temporary cache for {@link PackageItemInfos} to avoid having to go through // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList} PackageItemInfoCache packageItemInfoCache = new PackageItemInfoCache(); @@ -195,7 +230,6 @@ public class WidgetsModel { // add and update. mWidgetsByPackageItem.putAll(rawWidgetsShortcuts.stream() - .filter(new WidgetValidityCheck(app)) .filter(new WidgetFlagCheck()) .flatMap(widgetItem -> getPackageUserKeys(app.getContext(), widgetItem).stream() .map(key -> new Pair<>(packageItemInfoCache.getOrCreate(key), widgetItem))) @@ -270,12 +304,15 @@ public class WidgetsModel { return packageUserKeys; } - private static class WidgetValidityCheck implements Predicate { + /** + * Checks if widgets are eligible for displaying in widget picker / tray. + */ + private static class WidgetValidityCheckForPicker implements Predicate { private final InvariantDeviceProfile mIdp; private final AppFilter mAppFilter; - WidgetValidityCheck(LauncherAppState app) { + WidgetValidityCheckForPicker(LauncherAppState app) { mIdp = app.getInvariantDeviceProfile(); mAppFilter = new AppFilter(app.getContext()); } @@ -310,6 +347,10 @@ public class WidgetsModel { } } + /** + * Checks if certain widgets that are available behind flag can be used across all surfaces in + * launcher. + */ private static class WidgetFlagCheck implements Predicate { private static final String BUBBLES_SHORTCUT_WIDGET = diff --git a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt index ae4ff042d5..d704195052 100644 --- a/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/model/WidgetsModelTest.kt @@ -119,6 +119,11 @@ class WidgetsModelTest { // A widget in different package (none of that app's widgets are in widget // sections xml) createAppWidgetProviderInfo(AppBTestWidgetComponent), + // A widget in different app that is meant to be hidden from picker + createAppWidgetProviderInfo( + AppCPinOnlyTestWidgetComponent, + /*hideFromPicker=*/ true, + ), ) ) @@ -129,12 +134,13 @@ class WidgetsModelTest { } @Test - fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() { + fun widgetsByPackageForPicker_treatsWidgetSectionsAsSeparatePackageItems() { loadWidgets() - val packages: Map> = underTest.widgetsByPackageItem + val packages: Map> = + underTest.widgetsByPackageItemForPicker - // expect 3 package items + // expect 3 package items (no app C as its widget is hidden from picker) // one for the custom section with widget from appA // one for package section for second widget from appA (that wasn't listed in xml) // and one for package section for appB @@ -167,6 +173,13 @@ class WidgetsModelTest { assertThat(appBPackageSection).hasSize(1) val widgetsInAppBSection = appBPackageSection.entries.first().value assertThat(widgetsInAppBSection).hasSize(1) + + // No App C's package section - as the only widget hosted by it is hidden in picker + val appCPackageSection = + packageSections.filter { + it.key.packageName == AppCPinOnlyTestWidgetComponent.packageName + } + assertThat(appCPackageSection).isEmpty() } @Test @@ -175,7 +188,29 @@ class WidgetsModelTest { val widgetsByComponentKey: Map = underTest.widgetsByComponentKey + // Has all widgets including ones not visible in picker + assertThat(widgetsByComponentKey).hasSize(4) + widgetsByComponentKey.forEach { entry -> + assertThat(entry.key).isEqualTo(entry.value as ComponentKey) + } + } + + @Test + fun widgetComponentMapForPicker_excludesWidgetsHiddenInPicker() { + loadWidgets() + + val widgetsByComponentKey: Map = + underTest.widgetsByComponentKeyForPicker + + // Has all widgets excluding the appC's widget. assertThat(widgetsByComponentKey).hasSize(3) + assertThat( + widgetsByComponentKey.filter { + it.key.componentName == AppCPinOnlyTestWidgetComponent + } + ) + .isEmpty() + // widgets mapped correctly widgetsByComponentKey.forEach { entry -> assertThat(entry.key).isEqualTo(entry.value as ComponentKey) } @@ -189,7 +224,7 @@ class WidgetsModelTest { } @Test - fun getWidgetsByPackageItem_returnsACopyOfMap() { + fun getWidgetsByPackageItemForPicker_returnsACopyOfMap() { loadWidgets() val latch = CountDownLatch(1) @@ -198,8 +233,8 @@ class WidgetsModelTest { // each "widgetsByPackageItem" read returns a different copy of the map held internally. // Modifying one shouldn't impact another. - for ((_, _) in underTest.widgetsByPackageItem.entries) { - underTest.widgetsByPackageItem.clear() + for ((_, _) in underTest.widgetsByPackageItemForPicker.entries) { + underTest.widgetsByPackageItemForPicker.clear() if (update) { // trigger update update = false // Similarly, model could update its code independently while a client is @@ -256,6 +291,9 @@ class WidgetsModelTest { private val AppBTestWidgetComponent: ComponentName = ComponentName.createRelative("com.test.package", "TestProvider") + private val AppCPinOnlyTestWidgetComponent: ComponentName = + ComponentName.createRelative("com.testC.package", "PinOnlyTestProvider") + private const val LOAD_WIDGETS_TIMEOUT_SECONDS = 2L } } diff --git a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java index a87a208187..9fbd7ff311 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java +++ b/tests/multivalentTests/src/com/android/launcher3/util/WidgetUtils.java @@ -15,6 +15,8 @@ */ package com.android.launcher3.util; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_PICKER; + import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; @@ -83,14 +85,30 @@ public class WidgetUtils { /** * Creates a {@link AppWidgetProviderInfo} for the provided component name + * + * @param cn component name of the appwidget provider + * @param hideFromPicker indicates if the widget should appear in widget picker */ - public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) { + public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn, + boolean hideFromPicker) { ActivityInfo activityInfo = new ActivityInfo(); activityInfo.applicationInfo = new ApplicationInfo(); activityInfo.applicationInfo.uid = Process.myUid(); AppWidgetProviderInfo info = new AppWidgetProviderInfo(); + if (hideFromPicker) { + info.widgetFeatures = WIDGET_FEATURE_HIDE_FROM_PICKER; + } info.providerInfo = activityInfo; info.provider = cn; return info; } + + /** + * Creates a {@link AppWidgetProviderInfo} for the provided component name + * + * @param cn component name of the appwidget provider + */ + public static AppWidgetProviderInfo createAppWidgetProviderInfo(ComponentName cn) { + return createAppWidgetProviderInfo(cn, /*hideFromPicker=*/ false); + } }