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); + } }