Merge "Update widgetsModel to return pickable vs all widgets separately." into main

This commit is contained in:
Shamali Patwa
2025-02-20 17:42:32 -08:00
committed by Android (Google) Code Review
8 changed files with 151 additions and 20 deletions
@@ -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);
}
});
@@ -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<ComponentKey, WidgetItem> allEligibleWidgets =
dataModel.widgetsModel.getWidgetsByComponentKey()
dataModel.widgetsModel.getWidgetsByComponentKeyForPicker()
.entrySet()
.stream()
.filter(entry -> entry.getValue().widgetInfo != null
@@ -91,6 +91,7 @@ public final class WidgetsPredicationUpdateTaskTest {
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 @@ 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<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);
@@ -167,7 +167,7 @@ public class BaseLauncherBinder {
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();
@@ -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
@@ -70,6 +70,7 @@ public class WidgetsModel {
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 @@ 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<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 @@ 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<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 @@ 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<WidgetItem> {
private static final String BUBBLES_SHORTCUT_WIDGET =
@@ -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<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 @@ 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<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 @@ 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
}
}
@@ -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);
}
}