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
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 1cf7dda..f992913 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -322,13 +322,14 @@
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 40e1c10..9626a61 100644
--- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
+++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java
@@ -79,7 +79,7 @@
// Widgets (excluding shortcuts & already added widgets) that belong to apps eligible for
// being in predictions.
Map<ComponentKey, WidgetItem> 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 fa2eb1e..d52d054 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 @@
private AppWidgetProviderInfo mApp4Provider1;
private AppWidgetProviderInfo mApp4Provider2;
private AppWidgetProviderInfo mApp5Provider1;
+ private AppWidgetProviderInfo mApp6PinOnlyProvider1;
private List<AppWidgetProviderInfo> allWidgets;
private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
@@ -117,8 +118,14 @@
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 @@
});
}
+ @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<PendingAddWidgetInfo> 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 003bef3..3ee8b87 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -167,7 +167,7 @@
return;
}
Map<PackageItemInfo, List<WidgetItem>>
- widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItem();
+ widgetsByPackageItem = mBgDataModel.widgetsModel.getWidgetsByPackageItemForPicker();
List<WidgetsListBaseEntry> widgets = new WidgetsListBaseEntriesBuilder(mApp.getContext())
.build(widgetsByPackageItem);
Predicate<WidgetItem> filter = mBgDataModel.widgetsModel.getDefaultWidgetsFilter();
diff --git a/src/com/android/launcher3/model/ModelTaskController.kt b/src/com/android/launcher3/model/ModelTaskController.kt
index 40ea17d..6e3e35e 100644
--- a/src/com/android/launcher3/model/ModelTaskController.kt
+++ b/src/com/android/launcher3/model/ModelTaskController.kt
@@ -77,7 +77,7 @@
}
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 a176465..ab960d8 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -70,6 +70,7 @@
private final Map<PackageItemInfo, List<WidgetItem>> mWidgetsByPackageItem = new HashMap<>();
@Nullable private Predicate<WidgetItem> mDefaultWidgetsFilter = null;
@Nullable private Predicate<WidgetItem> mPredictedWidgetsFilter = null;
+ @Nullable private WidgetValidityCheckForPicker mWidgetValidityCheckForPicker = null;
/**
* Returns all widgets keyed by their component key.
@@ -87,13 +88,44 @@
}
/**
- * 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<PackageItemInfo, List<WidgetItem>> getWidgetsByPackageItem() {
- if (!WIDGETS_ENABLED) {
+ public synchronized Map<ComponentKey, WidgetItem> 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<PackageItemInfo, List<WidgetItem>> 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 @@
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 @@
// 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 @@
return packageUserKeys;
}
- private static class WidgetValidityCheck implements Predicate<WidgetItem> {
+ /**
+ * Checks if widgets are eligible for displaying in widget picker / tray.
+ */
+ private static class WidgetValidityCheckForPicker implements Predicate<WidgetItem> {
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 @@
}
}
+ /**
+ * Checks if certain widgets that are available behind flag can be used across all surfaces in
+ * launcher.
+ */
private static class WidgetFlagCheck implements Predicate<WidgetItem> {
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 ae4ff04..d704195 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 @@
// 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 @@
}
@Test
- fun widgetsByPackage_treatsWidgetSectionsAsSeparatePackageItems() {
+ fun widgetsByPackageForPicker_treatsWidgetSectionsAsSeparatePackageItems() {
loadWidgets()
- val packages: Map<PackageItemInfo, List<WidgetItem>> = underTest.widgetsByPackageItem
+ val packages: Map<PackageItemInfo, List<WidgetItem>> =
+ 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 @@
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 @@
val widgetsByComponentKey: Map<ComponentKey, WidgetItem> = 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<ComponentKey, WidgetItem> =
+ 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 @@
}
@Test
- fun getWidgetsByPackageItem_returnsACopyOfMap() {
+ fun getWidgetsByPackageItemForPicker_returnsACopyOfMap() {
loadWidgets()
val latch = CountDownLatch(1)
@@ -198,8 +233,8 @@
// 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 @@
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 a87a208..9fbd7ff 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 @@
/**
* 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);
+ }
}