Merge changes from topic "popup-refactor" into main
* changes:
Delete the unused code from popup provider
Update references that read popup data provider to use picker provider
Update references that wrote widget data to popup data provider
Move widget related listeners to widget picker data provider
Define widget picker data provider separate from popup provider
Create a separate class for widget related methods from popup provider
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
index 0b18633..e925af6 100644
--- a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -28,7 +28,6 @@
import android.appwidget.AppWidgetProviderInfo;
import android.content.ClipData;
import android.content.ClipDescription;
-import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@@ -48,11 +47,11 @@
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.PackageItemInfo;
-import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.model.WidgetsListBaseEntriesBuilder;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import java.util.ArrayList;
import java.util.HashSet;
@@ -110,8 +109,8 @@
private WidgetsModel mModel;
private LauncherAppState mApp;
private WidgetPredictionsRequester mWidgetPredictionsRequester;
- private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {
- });
+ private final WidgetPickerDataProvider mWidgetPickerDataProvider =
+ new WidgetPickerDataProvider();
private int mDesiredWidgetWidth;
private int mDesiredWidgetHeight;
@@ -215,8 +214,8 @@
@NonNull
@Override
- public PopupDataProvider getPopupDataProvider() {
- return mPopupDataProvider;
+ public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+ return mWidgetPickerDataProvider;
}
@Override
@@ -293,8 +292,6 @@
private void refreshAndBindWidgets() {
MODEL_EXECUTOR.execute(() -> {
LauncherAppState app = LauncherAppState.getInstance(this);
- Context context = app.getContext();
-
mModel.update(app, null);
bindWidgets(mModel.getWidgetsByPackageItem());
// Open sheet once widgets are available, so that it doesn't interrupt the open
@@ -317,7 +314,8 @@
shouldShowDefaultWidgets() ? builder.build(widgets,
mDefaultWidgetsFilter) : List.of();
- MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(allWidgets, defaultWidgets));
+ MAIN_EXECUTOR.execute(
+ () -> mWidgetPickerDataProvider.setWidgets(allWidgets, defaultWidgets));
}
private void openWidgetsSheet() {
@@ -332,7 +330,7 @@
private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
// Bind recommendations once picker has finished open animation.
MAIN_EXECUTOR.getHandler().postDelayed(
- () -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets),
+ () -> mWidgetPickerDataProvider.setWidgetRecommendations(recommendedWidgets),
mDeviceProfile.bottomSheetOpenDuration);
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 5176d74..bbc6b42 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -515,7 +515,7 @@
} else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
mHotseatPredictionController.setPredictedItems(item);
} else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
- getPopupDataProvider().setRecommendedWidgets(item.items);
+ getWidgetPickerDataProvider().setWidgetRecommendations(item.items);
}
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5949732..6c706be 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -266,6 +266,7 @@
import com.android.launcher3.widget.custom.CustomWidgetManager;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import com.android.launcher3.widget.util.WidgetSizes;
import com.android.systemui.plugins.LauncherOverlayPlugin;
import com.android.systemui.plugins.PluginListener;
@@ -371,6 +372,7 @@
private LauncherAccessibilityDelegate mAccessibilityDelegate;
private PopupDataProvider mPopupDataProvider;
+ private WidgetPickerDataProvider mWidgetPickerDataProvider;
// We only want to get the SharedPreferences once since it does an FS stat each time we get
// it from the context.
@@ -532,6 +534,7 @@
mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace));
mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
+ mWidgetPickerDataProvider = new WidgetPickerDataProvider();
boolean internalStateHandled = ACTIVITY_TRACKER.handleCreate(this);
if (internalStateHandled) {
@@ -2702,6 +2705,7 @@
mDragLayer.dump(prefix, writer);
mStateManager.dump(prefix, writer);
mPopupDataProvider.dump(prefix, writer);
+ mWidgetPickerDataProvider.dump(prefix, writer);
mDeviceProfile.dump(this, prefix, writer);
mAppsView.getAppsStore().dump(prefix, writer);
@@ -3011,6 +3015,12 @@
return mPopupDataProvider;
}
+ @NonNull
+ @Override
+ public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+ return mWidgetPickerDataProvider;
+ }
+
@Override
public DotInfo getDotInfoForItem(ItemInfo info) {
return mPopupDataProvider.getDotInfoForItem(info);
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 83c34ce..d57f8a0 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -254,8 +254,8 @@
PopupContainerWithArrow.dismissInvalidPopup(launcher)
}
- override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry?>?) {
- launcher.popupDataProvider.allWidgets = allWidgets
+ override fun bindAllWidgets(allWidgets: List<WidgetsListBaseEntry>) {
+ launcher.widgetPickerDataProvider.setWidgets(allWidgets, /* defaultWidgets= */ listOf())
}
/** Returns the ids of the workspaces to bind. */
@@ -304,7 +304,8 @@
}
val widgetsListBaseEntry: WidgetsListBaseEntry =
- launcher.popupDataProvider.allWidgets.firstOrNull { item: WidgetsListBaseEntry ->
+ launcher.widgetPickerDataProvider.get().allWidgets.firstOrNull {
+ item: WidgetsListBaseEntry ->
item.mPkgItem.packageName == BuildConfig.APPLICATION_ID
} ?: return
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 7e139c3..8a5e388 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -24,28 +24,20 @@
import androidx.annotation.Nullable;
import com.android.launcher3.dot.DotInfo;
-import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.notification.NotificationKeyData;
import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.ShortcutUtil;
-import com.android.launcher3.widget.PendingAddWidgetInfo;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.picker.WidgetRecommendationCategory;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.function.Consumer;
-import java.util.function.Function;
import java.util.function.Predicate;
-import java.util.stream.Collectors;
/**
* Provides data for the popup menu that appears after long-clicking on apps.
@@ -62,18 +54,6 @@
/** Maps packages to their DotInfo's . */
private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
- /** All installed widgets. */
- private List<WidgetsListBaseEntry> mAllWidgets = List.of();
- /**
- * Selectively chosen installed widgets which may be preferred for default display over the list
- * of all widgets.
- */
- private List<WidgetsListBaseEntry> mDefaultWidgets = List.of();
- /** Widgets that can be recommended to the users. */
- private List<ItemInfo> mRecommendedWidgets = List.of();
-
- private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE;
-
public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
mNotificationDotsChangeListener = notificationDotsChangeListener;
}
@@ -188,124 +168,8 @@
})) ? dotInfo : null;
}
- /**
- * Sets a list of recommended widgets ordered by their order of appearance in the widgets
- * recommendation UI.
- */
- public void setRecommendedWidgets(List<ItemInfo> recommendedWidgets) {
- mRecommendedWidgets = recommendedWidgets;
- mChangeListener.onRecommendedWidgetsBound();
- }
-
- public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets) {
- mAllWidgets = allWidgets;
- mDefaultWidgets = List.of();
- mChangeListener.onWidgetsBound();
- }
-
- /**
- * Sets the list of widgets to be displayed by default and a complete list that can be displayed
- * when user chooses to show all widgets.
- */
- public void setAllWidgets(List<WidgetsListBaseEntry> allWidgets,
- List<WidgetsListBaseEntry> defaultWidgets) {
- mAllWidgets = allWidgets;
- mDefaultWidgets = defaultWidgets;
- mChangeListener.onWidgetsBound();
- }
-
- public void setChangeListener(PopupDataChangeListener listener) {
- mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener;
- }
-
- public List<WidgetsListBaseEntry> getAllWidgets() {
- return mAllWidgets;
- }
-
- /**
- * Returns a "selectively" chosen list of widgets that may be preferred to be shown by default
- * instead of a complete list.
- */
- public List<WidgetsListBaseEntry> getDefaultWidgets() {
- return mDefaultWidgets;
- }
-
- /** Returns a list of recommended widgets. */
- public List<WidgetItem> getRecommendedWidgets() {
- HashMap<ComponentKey, WidgetItem> allWidgetItems = new HashMap<>();
- mAllWidgets.stream()
- .filter(entry -> entry instanceof WidgetsListContentEntry)
- .forEach(entry -> ((WidgetsListContentEntry) entry).mWidgets
- .forEach(widget -> allWidgetItems.put(
- new ComponentKey(widget.componentName, widget.user), widget)));
- return mRecommendedWidgets.stream()
- .map(recommendedWidget -> allWidgetItems.get(
- new ComponentKey(recommendedWidget.getTargetComponent(),
- recommendedWidget.user)))
- .filter(Objects::nonNull)
- .collect(Collectors.toList());
- }
-
- /** Returns the recommended widgets mapped by their category. */
- @NonNull
- public Map<WidgetRecommendationCategory, List<WidgetItem>> getCategorizedRecommendedWidgets() {
- Map<ComponentKey, WidgetItem> allWidgetItems = mAllWidgets.stream()
- .filter(entry -> entry instanceof WidgetsListContentEntry)
- .flatMap(entry -> entry.mWidgets.stream())
- .distinct()
- .collect(Collectors.toMap(
- widget -> new ComponentKey(widget.componentName, widget.user),
- Function.identity()
- ));
- return mRecommendedWidgets.stream()
- .filter(itemInfo -> itemInfo instanceof PendingAddWidgetInfo
- && ((PendingAddWidgetInfo) itemInfo).recommendationCategory != null)
- .collect(Collectors.groupingBy(
- it -> ((PendingAddWidgetInfo) it).recommendationCategory,
- Collectors.collectingAndThen(
- Collectors.toList(),
- list -> list.stream()
- .map(it -> allWidgetItems.get(
- new ComponentKey(it.getTargetComponent(),
- it.user)))
- .filter(Objects::nonNull)
- .collect(Collectors.toList())
- )
- ));
- }
-
- public List<WidgetItem> getWidgetsForPackageUser(PackageUserKey packageUserKey) {
- return mAllWidgets.stream()
- .filter(row -> row instanceof WidgetsListContentEntry
- && row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
- .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream())
- .filter(widget -> packageUserKey.mUser.equals(widget.user))
- .collect(Collectors.toList());
- }
-
- /** Gets the WidgetsListContentEntry for the currently selected header. */
- public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey,
- boolean useDefault) {
- List<WidgetsListBaseEntry> widgets = useDefault ? mDefaultWidgets : mAllWidgets;
- return (WidgetsListContentEntry) widgets.stream()
- .filter(row -> row instanceof WidgetsListContentEntry
- && PackageUserKey.fromPackageItemInfo(row.mPkgItem).equals(packageUserKey))
- .findAny()
- .orElse(null);
- }
-
public void dump(String prefix, PrintWriter writer) {
writer.println(prefix + "PopupDataProvider:");
writer.println(prefix + "\tmPackageUserToDotInfos:" + mPackageUserToDotInfos);
}
-
- public interface PopupDataChangeListener {
-
- PopupDataChangeListener INSTANCE = new PopupDataChangeListener() { };
-
- default void onWidgetsBound() { }
-
- /** A callback to get notified when recommended widgets are bound. */
- default void onRecommendedWidgetsBound() { }
- }
}
diff --git a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
index 4c94f94..1fd3557 100644
--- a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
+++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
@@ -19,6 +19,8 @@
import android.view.View;
import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;
/**
* Utility class to handle updates while the popup is visible (like widgets and
@@ -27,7 +29,7 @@
* @param <T> The activity on which the popup shows
*/
public abstract class PopupLiveUpdateHandler<T extends Context & ActivityContext> implements
- PopupDataProvider.PopupDataChangeListener, View.OnAttachStateChangeListener {
+ WidgetPickerDataChangeListener, View.OnAttachStateChangeListener {
protected final T mContext;
protected final PopupContainerWithArrow<T> mPopupContainerWithArrow;
@@ -40,19 +42,25 @@
@Override
public void onViewAttachedToWindow(View view) {
- PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+ WidgetPickerDataProvider widgetsDataProvider = mContext.getWidgetPickerDataProvider();
- if (popupDataProvider != null) {
- popupDataProvider.setChangeListener(this);
+ if (widgetsDataProvider != null) {
+ widgetsDataProvider.setChangeListener(this);
}
}
@Override
public void onViewDetachedFromWindow(View view) {
- PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+ WidgetPickerDataProvider widgetsDataProvider = mContext.getWidgetPickerDataProvider();
- if (popupDataProvider != null) {
- popupDataProvider.setChangeListener(null);
+ if (widgetsDataProvider != null) {
+ widgetsDataProvider.setChangeListener(null);
}
}
+
+ @Override
+ public void onWidgetsBound() {} // NO_OP
+
+ @Override
+ public void onRecommendedWidgetsBound() {} // NO_OP
}
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 83e9810..f7e1168 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -5,6 +5,7 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_DONT_SUGGEST_APP_TAP;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_WIDGETS_TAP;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser;
import android.app.ActivityOptions;
import android.content.ComponentName;
@@ -28,7 +29,6 @@
import com.android.launcher3.SecondaryDropTarget;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.PrivateProfileManager;
-import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.UserCache;
@@ -39,9 +39,9 @@
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.WidgetsBottomSheet;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
import java.util.Arrays;
-import java.util.List;
/**
* Represents a system shortcut for a given app. The shortcut should have a label and icon, and an
@@ -107,11 +107,12 @@
}
public static final Factory<ActivityContext> WIDGETS = (context, itemInfo, originalView) -> {
- if (itemInfo.getTargetComponent() == null) return null;
- final List<WidgetItem> widgets =
- context.getPopupDataProvider().getWidgetsForPackageUser(new PackageUserKey(
- itemInfo.getTargetComponent().getPackageName(), itemInfo.user));
- if (widgets.isEmpty()) {
+ final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(itemInfo);
+ if (packageUserKey == null) return null;
+
+ final WidgetPickerData data = context.getWidgetPickerDataProvider().get();
+ if (findAllWidgetsForPackageUser(data, packageUserKey).isEmpty()) {
+ // hides widget picker shortcut if there are no widgets for the package.
return null;
}
return new Widgets(context, itemInfo, originalView);
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 0299a23..9b3292d 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -59,6 +59,7 @@
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import java.util.HashMap;
import java.util.Map;
@@ -76,6 +77,7 @@
private View mAppsButton;
private PopupDataProvider mPopupDataProvider;
+ private WidgetPickerDataProvider mWidgetPickerDataProvider;
private boolean mAppDrawerShown = false;
@@ -315,6 +317,11 @@
}
@Override
+ public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+ return mWidgetPickerDataProvider;
+ }
+
+ @Override
public OnClickListener getItemOnClickListener() {
return this::onIconClicked;
}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index cfac91a..d3160e0 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -81,6 +81,7 @@
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.ViewCache;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import java.util.List;
@@ -266,6 +267,14 @@
return null;
}
+ /**
+ * Returns the {@link WidgetPickerDataProvider} that can be used to read widgets for display.
+ */
+ @Nullable
+ default WidgetPickerDataProvider getWidgetPickerDataProvider() {
+ return null;
+ }
+
@Nullable
default StringCache getStringCache() {
return null;
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index c59e295..1c0d94c 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -45,13 +45,13 @@
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
-import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.launcher3.views.AbstractSlideInView;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -60,7 +60,7 @@
*/
public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
implements OnClickListener, OnLongClickListener,
- PopupDataProvider.PopupDataChangeListener, Insettable, OnDeviceProfileChangeListener {
+ WidgetPickerDataChangeListener, Insettable, OnDeviceProfileChangeListener {
/** The default number of cells that can fit horizontally in a widget sheet. */
public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4;
@@ -106,14 +106,14 @@
WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext())
.normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect());
mNavBarScrimHeight = getNavBarScrimHeight(windowInsets);
- mActivityContext.getPopupDataProvider().setChangeListener(this);
+ mActivityContext.getWidgetPickerDataProvider().setChangeListener(this);
mActivityContext.addOnDeviceProfileChangeListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- mActivityContext.getPopupDataProvider().setChangeListener(null);
+ mActivityContext.getWidgetPickerDataProvider().setChangeListener(null);
mActivityContext.removeOnDeviceProfileChangeListener(this);
}
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 894099d..ddbd291 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -17,6 +17,7 @@
package com.android.launcher3.widget;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_BOTTOM_WIDGETS_TRAY;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser;
import android.content.Context;
import android.graphics.Rect;
@@ -40,6 +41,7 @@
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
import com.android.launcher3.widget.util.WidgetsTableUtils;
import java.util.List;
@@ -124,10 +126,10 @@
@Override
public void onWidgetsBound() {
- List<WidgetItem> widgets = mActivityContext.getPopupDataProvider().getWidgetsForPackageUser(
- new PackageUserKey(
- mOriginalItemInfo.getTargetComponent().getPackageName(),
- mOriginalItemInfo.user));
+ final WidgetPickerData data = mActivityContext.getWidgetPickerDataProvider().get();
+ final PackageUserKey packageUserKey = PackageUserKey.fromItemInfo(mOriginalItemInfo);
+ List<WidgetItem> widgets = packageUserKey != null ? findAllWidgetsForPackageUser(data,
+ packageUserKey) : List.of();
TableLayout widgetsTable = findViewById(R.id.widgets_table);
widgetsTable.removeAllViews();
@@ -247,4 +249,7 @@
}
}
}
+
+ @Override
+ public void onRecommendedWidgetsBound() {} // no op
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
index 072d1d5..a68effd 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategory.java
@@ -19,6 +19,8 @@
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import com.android.launcher3.R;
+
import java.util.Objects;
/**
@@ -26,6 +28,10 @@
* option in the pop-up opened on long press of launcher workspace).
*/
public class WidgetRecommendationCategory implements Comparable<WidgetRecommendationCategory> {
+ public static WidgetRecommendationCategory DEFAULT_WIDGET_RECOMMENDATION_CATEGORY =
+ new WidgetRecommendationCategory(
+ R.string.others_widget_recommendation_category_label, /*order=*/0);
+
/** Resource id that holds the user friendly label for the category. */
@StringRes
public final int categoryTitleRes;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 21b7be4..2af8e6f 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -74,6 +74,7 @@
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -471,7 +472,7 @@
* Returns all displayable widgets.
*/
protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
- return mActivityContext.getPopupDataProvider().getAllWidgets();
+ return mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
}
@Override
@@ -572,12 +573,11 @@
if (mIsInSearchMode) {
return;
}
-
if (enableCategorizedWidgetSuggestions()) {
// We avoid applying new recommendations when some are already displayed.
if (mRecommendedWidgetsMap.isEmpty()) {
mRecommendedWidgetsMap =
- mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets();
+ mActivityContext.getWidgetPickerDataProvider().get().getRecommendations();
}
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
mRecommendedWidgetsMap,
@@ -589,17 +589,20 @@
);
} else {
if (mRecommendedWidgets.isEmpty()) {
- mRecommendedWidgets =
- mActivityContext.getPopupDataProvider().getRecommendedWidgets();
+ mRecommendedWidgets = mActivityContext.getWidgetPickerDataProvider().get()
+ .getRecommendations()
+ .values().stream()
+ .flatMap(Collection::stream).toList();
+ mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
+ mRecommendedWidgets,
+ mDeviceProfile,
+ /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
+ /* availableWidth= */ mMaxSpanPerRow,
+ /* cellPadding= */ mWidgetCellHorizontalPadding
+ );
}
- mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
- mRecommendedWidgets,
- mDeviceProfile,
- /* availableHeight= */ getMaxAvailableHeightForRecommendations(),
- /* availableWidth= */ mMaxSpanPerRow,
- /* cellPadding= */ mWidgetCellHorizontalPadding
- );
}
+
mWidgetRecommendationsContainer.setVisibility(
mRecommendedWidgetsCount > 0 ? VISIBLE : GONE);
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index c4c755a..c2cd903 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -22,6 +22,7 @@
import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.WIDGET_LIST_ITEM_APPEARANCE_DELAY;
+import static com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser;
import android.content.Context;
import android.graphics.Rect;
@@ -287,9 +288,9 @@
@Override
protected List<WidgetsListBaseEntry> getWidgetsToDisplay() {
List<WidgetsListBaseEntry> allWidgets =
- mActivityContext.getPopupDataProvider().getAllWidgets();
+ mActivityContext.getWidgetPickerDataProvider().get().getAllWidgets();
List<WidgetsListBaseEntry> defaultWidgets =
- mActivityContext.getPopupDataProvider().getDefaultWidgets();
+ mActivityContext.getWidgetPickerDataProvider().get().getDefaultWidgets();
if (allWidgets.isEmpty() || defaultWidgets.isEmpty()) {
// no menu if there are no default widgets to show
@@ -359,7 +360,7 @@
WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
packageItemInfo,
/*titleSectionName=*/ suggestionsHeaderTitle,
- /*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+ /*items=*/ List.of(), // not necessary
/*visibleWidgetsCount=*/ 0)
.withWidgetListShown();
@@ -509,11 +510,11 @@
final boolean isUserClick = mSelectedHeader != null
&& !getAccessibilityInitialFocusView().isAccessibilityFocused();
mSelectedHeader = selectedHeader;
- WidgetsListContentEntry contentEntry =
- mActivityContext.getPopupDataProvider().getSelectedAppWidgets(
- selectedHeader, /*useDefault=*/
- (mWidgetOptionsMenuState != null
- && !mWidgetOptionsMenuState.showAllWidgets));
+ final boolean showDefaultWidgets = mWidgetOptionsMenuState != null
+ && !mWidgetOptionsMenuState.showAllWidgets;
+ WidgetsListContentEntry contentEntry = findContentEntryForPackageUser(
+ mActivityContext.getWidgetPickerDataProvider().get(),
+ selectedHeader, showDefaultWidgets);
if (contentEntry == null || mRightPane == null) {
return;
diff --git a/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt
new file mode 100644
index 0000000..46d3e7a
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProvider.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model
+
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
+import java.io.PrintWriter
+
+/**
+ * Provides [WidgetPickerData] to various views such as widget picker, app-specific widget picker,
+ * widgets shortcut.
+ */
+class WidgetPickerDataProvider {
+ /** All the widgets data provided for the views */
+ private var mWidgetPickerData: WidgetPickerData = WidgetPickerData()
+
+ private var changeListener: WidgetPickerDataChangeListener? = null
+
+ /** Sets a listener to be called back when widget data is updated. */
+ fun setChangeListener(changeListener: WidgetPickerDataChangeListener?) {
+ this.changeListener = changeListener
+ }
+
+ /** Returns the current snapshot of [WidgetPickerData]. */
+ fun get(): WidgetPickerData {
+ return mWidgetPickerData
+ }
+
+ /**
+ * Updates the widgets available to the widget picker.
+ *
+ * Generally called when the widgets model has new data.
+ */
+ @JvmOverloads
+ fun setWidgets(
+ allWidgets: List<WidgetsListBaseEntry>,
+ defaultWidgets: List<WidgetsListBaseEntry> = listOf()
+ ) {
+ mWidgetPickerData =
+ mWidgetPickerData.withWidgets(allWidgets = allWidgets, defaultWidgets = defaultWidgets)
+ changeListener?.onWidgetsBound()
+ }
+
+ /**
+ * Makes the widget recommendations available to the widget picker
+ *
+ * Generally called when new widget predictions are available.
+ */
+ fun setWidgetRecommendations(recommendations: List<ItemInfo>) {
+ mWidgetPickerData = mWidgetPickerData.withRecommendedWidgets(recommendations)
+ changeListener?.onRecommendedWidgetsBound()
+ }
+
+ /** Writes the current state to the provided writer. */
+ fun dump(prefix: String, writer: PrintWriter) {
+ writer.println(prefix + "WidgetPickerDataProvider:")
+ writer.println("$prefix\twidgetPickerData:$mWidgetPickerData")
+ }
+
+ interface WidgetPickerDataChangeListener {
+ /** A callback to get notified when widgets are bound. */
+ fun onWidgetsBound()
+
+ /** A callback to get notified when recommended widgets are bound. */
+ fun onRecommendedWidgetsBound()
+ }
+}
diff --git a/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt
new file mode 100644
index 0000000..3332ef0
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/model/data/WidgetPickerData.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model.data
+
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
+
+// This file contains WidgetPickerData and utility functions to operate on it.
+
+/** Widget data for display in the widget picker. */
+data class WidgetPickerData(
+ val allWidgets: List<WidgetsListBaseEntry> = listOf(),
+ val defaultWidgets: List<WidgetsListBaseEntry> = listOf(),
+ val recommendations: Map<WidgetRecommendationCategory, List<WidgetItem>> = mapOf(),
+)
+
+/** Provides utility methods to work with a [WidgetPickerData] object. */
+object WidgetPickerDataUtils {
+ /**
+ * Returns a [WidgetPickerData] with the provided widgets.
+ *
+ * When [defaultWidgets] is not passed, defaults from previous object are not copied over.
+ * Defaults (if supported) should be updated when all widgets are updated.
+ */
+ fun WidgetPickerData.withWidgets(
+ allWidgets: List<WidgetsListBaseEntry>,
+ defaultWidgets: List<WidgetsListBaseEntry> = listOf()
+ ): WidgetPickerData {
+ return copy(allWidgets = allWidgets, defaultWidgets = defaultWidgets)
+ }
+
+ /** Returns a [WidgetPickerData] with the given recommendations set. */
+ fun WidgetPickerData.withRecommendedWidgets(recommendations: List<ItemInfo>): WidgetPickerData {
+ val allWidgetsMap: Map<ComponentKey, WidgetItem> =
+ allWidgets
+ .filterIsInstance<WidgetsListContentEntry>()
+ .flatMap { it.mWidgets }
+ .filterNotNull()
+ .distinct()
+ .associateBy { it } // as ComponentKey
+
+ val categoriesMap =
+ recommendations
+ .filterIsInstance<PendingAddWidgetInfo>()
+ .filter { allWidgetsMap.containsKey(ComponentKey(it.targetComponent, it.user)) }
+ .groupBy { it.recommendationCategory ?: DEFAULT_WIDGET_RECOMMENDATION_CATEGORY }
+ .mapValues { (_, pendingAddWidgetInfos) ->
+ pendingAddWidgetInfos.map {
+ allWidgetsMap[ComponentKey(it.targetComponent, it.user)] as WidgetItem
+ }
+ }
+
+ return copy(recommendations = categoriesMap)
+ }
+
+ /** Finds all [WidgetItem]s available for the provided package user. */
+ @JvmStatic
+ fun findAllWidgetsForPackageUser(
+ widgetPickerData: WidgetPickerData,
+ packageUserKey: PackageUserKey
+ ): List<WidgetItem> {
+ return findContentEntryForPackageUser(widgetPickerData, packageUserKey)?.mWidgets
+ ?: emptyList()
+ }
+
+ /**
+ * Finds and returns the [WidgetsListContentEntry] for the given package user.
+ *
+ * Set [fromDefaultWidgets] to true to limit the content entry to default widgets.
+ */
+ @JvmOverloads
+ @JvmStatic
+ fun findContentEntryForPackageUser(
+ widgetPickerData: WidgetPickerData,
+ packageUserKey: PackageUserKey,
+ fromDefaultWidgets: Boolean = false
+ ): WidgetsListContentEntry? {
+ val widgetsListBaseEntries =
+ if (fromDefaultWidgets) {
+ widgetPickerData.defaultWidgets
+ } else {
+ widgetPickerData.allWidgets
+ }
+
+ return widgetsListBaseEntries.filterIsInstance<WidgetsListContentEntry>().firstOrNull {
+ PackageUserKey.fromPackageItemInfo(it.mPkgItem) == packageUserKey
+ }
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
index 3f37563..71637f1 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
@@ -32,6 +32,7 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
@@ -57,6 +58,8 @@
protected ActivityAllAppsContainerView<ActivityContextWrapper> mAppsView;
private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+ private final WidgetPickerDataProvider mWidgetPickerDataProvider =
+ new WidgetPickerDataProvider();
protected final UserCache mUserCache;
public TestSandboxModelContextWrapper(SandboxContext base) {
@@ -76,12 +79,19 @@
mAppsList = mAppsView.getPersonalAppList();
mAllAppsStore = mAppsView.getAppsStore();
}
+
@Nullable
@Override
public PopupDataProvider getPopupDataProvider() {
return mPopupDataProvider;
}
+ @Nullable
+ @Override
+ public WidgetPickerDataProvider getWidgetPickerDataProvider() {
+ return mWidgetPickerDataProvider;
+ }
+
@Override
public ActivityAllAppsContainerView<ActivityContextWrapper> getAppsView() {
return mAppsView;
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
new file mode 100644
index 0000000..1822639
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/WidgetPickerDataProviderTest.kt
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+// Tests for the WidgetPickerDataProvider class
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetPickerDataProviderTest {
+ @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+ @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var changeListener: WidgetPickerDataChangeListener
+
+ @Mock private lateinit var iconCache: IconCache
+
+ private lateinit var userHandle: UserHandle
+ private lateinit var context: Context
+ private lateinit var testInvariantProfile: InvariantDeviceProfile
+
+ private lateinit var appWidgetItem: WidgetItem
+
+ private var underTest = WidgetPickerDataProvider()
+
+ @Before
+ fun setUp() {
+ userHandle = UserHandle.CURRENT
+ context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ testInvariantProfile = LauncherAppState.getIDP(context)
+
+ doAnswer { invocation: InvocationOnMock ->
+ val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+ componentWithLabel.getComponent().shortClassName
+ }
+ .`when`(iconCache)
+ .getTitleNoCache(any<ComponentWithLabel>())
+
+ appWidgetItem = createWidgetItem()
+ }
+
+ @Test
+ fun setWidgets_invokesTheListener_andUpdatedWidgetsAvailable() {
+ assertThat(underTest.get().allWidgets).isEmpty()
+
+ underTest.setChangeListener(changeListener)
+ val allWidgets = appWidgetListBaseEntries()
+ underTest.setWidgets(allWidgets = allWidgets)
+
+ assertThat(underTest.get().allWidgets).containsExactlyElementsIn(allWidgets)
+ verify(changeListener, times(1)).onWidgetsBound()
+ verifyNoMoreInteractions(changeListener)
+ }
+
+ @Test
+ fun setWidgetRecommendations_callsBackTheListener_andUpdatedRecommendationsAvailable() {
+ underTest.setWidgets(allWidgets = appWidgetListBaseEntries())
+ assertThat(underTest.get().recommendations).isEmpty()
+
+ underTest.setChangeListener(changeListener)
+ val recommendations =
+ listOf(
+ PendingAddWidgetInfo(
+ appWidgetItem.widgetInfo,
+ LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+ ),
+ )
+ underTest.setWidgetRecommendations(recommendations)
+
+ assertThat(underTest.get().recommendations).hasSize(1)
+ verify(changeListener, times(1)).onRecommendedWidgetsBound()
+ verifyNoMoreInteractions(changeListener)
+ }
+
+ @Test
+ fun setChangeListener_null_noCallback() {
+ underTest.setChangeListener(changeListener)
+ underTest.setChangeListener(null) // reset
+
+ underTest.setWidgets(allWidgets = appWidgetListBaseEntries())
+ val recommendations =
+ listOf(
+ PendingAddWidgetInfo(
+ appWidgetItem.widgetInfo,
+ LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+ ),
+ )
+ underTest.setWidgetRecommendations(recommendations)
+
+ verifyNoMoreInteractions(changeListener)
+ }
+
+ private fun createWidgetItem(): WidgetItem {
+ val providerInfo =
+ WidgetUtils.createAppWidgetProviderInfo(
+ ComponentName.createRelative(APP_PACKAGE_NAME, APP_PROVIDER_1_CLASS_NAME)
+ )
+ val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+ return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+ }
+
+ private fun appWidgetListBaseEntries(): List<WidgetsListBaseEntry> {
+ val packageItemInfo = PackageItemInfo(APP_PACKAGE_NAME, userHandle)
+ packageItemInfo.title = APP_PACKAGE_TITLE
+ val widgets = listOf(appWidgetItem)
+
+ return buildList {
+ add(WidgetsListHeaderEntry.create(packageItemInfo, APP_SECTION_NAME, widgets))
+ add(WidgetsListContentEntry(packageItemInfo, APP_SECTION_NAME, widgets))
+ }
+ }
+
+ companion object {
+ const val APP_PACKAGE_NAME = "com.example.app"
+ const val APP_PACKAGE_TITLE = "SomeApp"
+ const val APP_SECTION_NAME = "S" // for fast popup
+ const val APP_PROVIDER_1_CLASS_NAME = "appProvider1"
+ }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
new file mode 100644
index 0000000..e59e211
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/picker/model/data/WidgetPickerDataTest.kt
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.widget.picker.model.data
+
+import android.content.ComponentName
+import android.content.Context
+import android.os.UserHandle
+import android.platform.test.rule.AllowedDevices
+import android.platform.test.rule.DeviceProduct
+import android.platform.test.rule.LimitDevicesRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION
+import com.android.launcher3.icons.ComponentWithLabel
+import com.android.launcher3.icons.IconCache
+import com.android.launcher3.model.WidgetItem
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.PackageItemInfo
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.PackageUserKey
+import com.android.launcher3.util.WidgetUtils
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
+import com.android.launcher3.widget.PendingAddWidgetInfo
+import com.android.launcher3.widget.model.WidgetsListBaseEntry
+import com.android.launcher3.widget.model.WidgetsListContentEntry
+import com.android.launcher3.widget.model.WidgetsListHeaderEntry
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory
+import com.android.launcher3.widget.picker.WidgetRecommendationCategory.DEFAULT_WIDGET_RECOMMENDATION_CATEGORY
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findAllWidgetsForPackageUser
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.findContentEntryForPackageUser
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withRecommendedWidgets
+import com.android.launcher3.widget.picker.model.data.WidgetPickerDataUtils.withWidgets
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+
+// Tests for code / classes in WidgetPickerData file.
+
+@RunWith(AndroidJUnit4::class)
+@AllowedDevices(allowed = [DeviceProduct.ROBOLECTRIC])
+class WidgetPickerDataTest {
+ @Rule @JvmField val limitDevicesRule = LimitDevicesRule()
+ @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var iconCache: IconCache
+
+ private lateinit var userHandle: UserHandle
+ private lateinit var context: Context
+ private lateinit var testInvariantProfile: InvariantDeviceProfile
+
+ private lateinit var app1PackageItemInfo: PackageItemInfo
+ private lateinit var app2PackageItemInfo: PackageItemInfo
+
+ private lateinit var app1WidgetItem1: WidgetItem
+ private lateinit var app1WidgetItem2: WidgetItem
+ private lateinit var app2WidgetItem1: WidgetItem
+
+ @Before
+ fun setUp() {
+ userHandle = UserHandle.CURRENT
+ context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+ testInvariantProfile = LauncherAppState.getIDP(context)
+
+ doAnswer { invocation: InvocationOnMock ->
+ val componentWithLabel = invocation.getArgument<Any>(0) as ComponentWithLabel
+ componentWithLabel.getComponent().shortClassName
+ }
+ .`when`(iconCache)
+ .getTitleNoCache(any<ComponentWithLabel>())
+
+ app1PackageItemInfo = packageItemInfoWithTitle(APP_1_PACKAGE_NAME, APP_1_PACKAGE_TITLE)
+ app2PackageItemInfo = packageItemInfoWithTitle(APP_2_PACKAGE_NAME, APP_2_PACKAGE_TITLE)
+
+ app1WidgetItem1 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_1_CLASS_NAME)
+ app1WidgetItem2 = createWidgetItem(APP_1_PACKAGE_NAME, APP_1_PROVIDER_2_CLASS_NAME)
+ app2WidgetItem1 = createWidgetItem(APP_2_PACKAGE_NAME, APP_2_PROVIDER_1_CLASS_NAME)
+ }
+
+ @Test
+ fun withWidgets_returnsACopyWithProvidedWidgets() {
+ // only app two
+ val widgetPickerData = WidgetPickerData(allWidgets = appTwoWidgetsListBaseEntries())
+
+ // update: only app 1 and default list set
+ val newAllWidgets: List<WidgetsListBaseEntry> =
+ appOneWidgetsListBaseEntries(includeWidgetTwo = true)
+ val newDefaultWidgets: List<WidgetsListBaseEntry> =
+ appOneWidgetsListBaseEntries(includeWidgetTwo = false)
+
+ val newWidgetData = widgetPickerData.withWidgets(newAllWidgets, newDefaultWidgets)
+
+ assertThat(newWidgetData.allWidgets).containsExactlyElementsIn(newAllWidgets)
+ assertThat(newWidgetData.defaultWidgets).containsExactlyElementsIn(newDefaultWidgets)
+ }
+
+ @Test
+ fun withWidgets_noExplicitDefaults_unsetsOld() {
+ // only app two
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets = appTwoWidgetsListBaseEntries(),
+ defaultWidgets = appTwoWidgetsListBaseEntries()
+ )
+
+ val newWidgetData =
+ widgetPickerData.withWidgets(allWidgets = appOneWidgetsListBaseEntries())
+
+ assertThat(newWidgetData.allWidgets)
+ .containsExactlyElementsIn(appOneWidgetsListBaseEntries())
+ assertThat(newWidgetData.defaultWidgets).isEmpty() // previous values cleared.
+ }
+
+ @Test
+ fun withRecommendedWidgets_returnsACopyWithProvidedRecommendedWidgets() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries())
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets = buildList { appTwoWidgetsListBaseEntries() }
+ )
+ val recommendations: List<ItemInfo> =
+ listOf(
+ PendingAddWidgetInfo(
+ app1WidgetItem1.widgetInfo,
+ CONTAINER_WIDGETS_PREDICTION,
+ CATEGORY_1
+ ),
+ PendingAddWidgetInfo(
+ app2WidgetItem1.widgetInfo,
+ CONTAINER_WIDGETS_PREDICTION,
+ CATEGORY_2
+ ),
+ )
+
+ val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+ assertThat(updatedData.recommendations.keys).containsExactly(CATEGORY_1, CATEGORY_2)
+ assertThat(updatedData.recommendations[CATEGORY_1]).containsExactly(app1WidgetItem1)
+ assertThat(updatedData.recommendations[CATEGORY_2]).containsExactly(app2WidgetItem1)
+ }
+
+ @Test
+ fun withRecommendedWidgets_noCategory_usesDefault() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries())
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets = buildList { appTwoWidgetsListBaseEntries() }
+ )
+ val recommendations: List<ItemInfo> =
+ listOf(
+ PendingAddWidgetInfo(app1WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+ PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+ )
+
+ val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+ assertThat(updatedData.recommendations.keys)
+ .containsExactly(DEFAULT_WIDGET_RECOMMENDATION_CATEGORY)
+ assertThat(updatedData.recommendations[DEFAULT_WIDGET_RECOMMENDATION_CATEGORY])
+ .containsExactly(app1WidgetItem1, app2WidgetItem1)
+ }
+
+ @Test
+ fun withRecommendedWidgets_emptyRecommendations_clearsOld() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries())
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+ recommendations = mapOf(CATEGORY_1 to listOf(app1WidgetItem1))
+ )
+
+ val updatedData = widgetPickerData.withRecommendedWidgets(listOf())
+
+ assertThat(updatedData.recommendations).isEmpty()
+ }
+
+ @Test
+ fun withRecommendedWidgets_widgetNotInAllWidgets_filteredOut() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false))
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets = buildList { appTwoWidgetsListBaseEntries() },
+ )
+
+ val recommendations: List<ItemInfo> =
+ listOf(
+ PendingAddWidgetInfo(app1WidgetItem2.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+ PendingAddWidgetInfo(app2WidgetItem1.widgetInfo, CONTAINER_WIDGETS_PREDICTION),
+ )
+ val updatedData = widgetPickerData.withRecommendedWidgets(recommendations)
+
+ assertThat(updatedData.recommendations).hasSize(1)
+ // no app1widget2
+ assertThat(updatedData.recommendations.values.first()).containsExactly(app2WidgetItem1)
+ }
+
+ @Test
+ fun findContentEntryForPackageUser_returnsCorrectEntry() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries())
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) }
+ )
+ val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+ val contentEntry = findContentEntryForPackageUser(widgetPickerData, app1PackageUserKey)
+
+ assertThat(contentEntry).isNotNull()
+ assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
+ assertThat(contentEntry?.mWidgets).hasSize(2)
+ }
+
+ @Test
+ fun findContentEntryForPackageUser_fromDefaults_returnsEntryFromDefaultWidgets() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries())
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets =
+ buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) }
+ )
+ val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+ val contentEntry =
+ findContentEntryForPackageUser(
+ widgetPickerData = widgetPickerData,
+ packageUserKey = app1PackageUserKey,
+ fromDefaultWidgets = true
+ )
+
+ assertThat(contentEntry).isNotNull()
+ assertThat(contentEntry?.mPkgItem).isEqualTo(app1PackageItemInfo)
+ // only one widget (since default widgets had only one widget for app A
+ assertThat(contentEntry?.mWidgets).hasSize(1)
+ }
+
+ @Test
+ fun findContentEntryForPackageUser_noMatch_returnsNull() {
+ val app2PackageUserKey = PackageUserKey.fromPackageItemInfo(app2PackageItemInfo)
+ val widgetPickerData =
+ WidgetPickerData(allWidgets = buildList { addAll(appOneWidgetsListBaseEntries()) })
+
+ val contentEntry = findContentEntryForPackageUser(widgetPickerData, app2PackageUserKey)
+
+ assertThat(contentEntry).isNull()
+ }
+
+ @Test
+ fun findAllWidgetsForPackageUser_returnsListOfWidgets() {
+ val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets =
+ buildList {
+ addAll(appOneWidgetsListBaseEntries())
+ addAll(appTwoWidgetsListBaseEntries())
+ },
+ defaultWidgets =
+ buildList { addAll(appOneWidgetsListBaseEntries(includeWidgetTwo = false)) }
+ )
+
+ val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
+
+ // both widgets returned irrespective of default widgets list
+ assertThat(widgets).hasSize(2)
+ }
+
+ @Test
+ fun findAllWidgetsForPackageUser_noMatch_returnsEmptyList() {
+ val widgetPickerData =
+ WidgetPickerData(
+ allWidgets = buildList { addAll(appTwoWidgetsListBaseEntries()) },
+ )
+ val app1PackageUserKey = PackageUserKey.fromPackageItemInfo(app1PackageItemInfo)
+
+ val widgets = findAllWidgetsForPackageUser(widgetPickerData, app1PackageUserKey)
+
+ assertThat(widgets).isEmpty()
+ }
+
+ private fun packageItemInfoWithTitle(packageName: String, title: String): PackageItemInfo {
+ val packageItemInfo = PackageItemInfo(packageName, userHandle)
+ packageItemInfo.title = title
+ return packageItemInfo
+ }
+
+ private fun createWidgetItem(packageName: String, widgetProviderName: String): WidgetItem {
+ val providerInfo =
+ WidgetUtils.createAppWidgetProviderInfo(
+ ComponentName.createRelative(packageName, widgetProviderName)
+ )
+ val widgetInfo = LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo)
+ return WidgetItem(widgetInfo, testInvariantProfile, iconCache, context)
+ }
+
+ private fun appTwoWidgetsListBaseEntries(): List<WidgetsListBaseEntry> = buildList {
+ val widgets = listOf(app2WidgetItem1)
+ add(WidgetsListHeaderEntry.create(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
+ add(WidgetsListContentEntry(app2PackageItemInfo, APP_2_SECTION_NAME, widgets))
+ }
+
+ private fun appOneWidgetsListBaseEntries(
+ includeWidgetTwo: Boolean = true
+ ): List<WidgetsListBaseEntry> = buildList {
+ val widgets =
+ if (includeWidgetTwo) {
+ listOf(app1WidgetItem1, app1WidgetItem2)
+ } else {
+ listOf(app1WidgetItem1)
+ }
+
+ add(WidgetsListHeaderEntry.create(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
+ add(WidgetsListContentEntry(app1PackageItemInfo, APP_1_SECTION_NAME, widgets))
+ }
+
+ companion object {
+ private const val APP_1_PACKAGE_NAME = "com.example.app1"
+ private const val APP_1_PACKAGE_TITLE = "App1"
+ private const val APP_1_SECTION_NAME = "A" // for fast popup
+ private const val APP_1_PROVIDER_1_CLASS_NAME = "app1Provider1"
+ private const val APP_1_PROVIDER_2_CLASS_NAME = "app1Provider2"
+
+ private const val APP_2_PACKAGE_NAME = "com.example.app2"
+ private const val APP_2_PACKAGE_TITLE = "SomeApp2"
+ private const val APP_2_SECTION_NAME = "S" // for fast popup
+ private const val APP_2_PROVIDER_1_CLASS_NAME = "app2Provider1"
+
+ private val CATEGORY_1 =
+ WidgetRecommendationCategory(/* categoryTitleRes= */ 0, /* order= */ 0)
+ private val CATEGORY_2 =
+ WidgetRecommendationCategory(/* categoryTitleRes= */ 1, /* order= */ 1)
+ }
+}
diff --git a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
index 98b6b4b..dcfcad5 100644
--- a/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
+++ b/tests/src/com/android/launcher3/popup/SystemShortcutTest.java
@@ -63,6 +63,8 @@
import com.android.launcher3.util.TestSandboxModelContextWrapper;
import com.android.launcher3.util.UserIconInfo;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider;
+import com.android.launcher3.widget.picker.model.data.WidgetPickerData;
import org.junit.After;
import org.junit.Assert;
@@ -73,8 +75,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.util.ArrayList;
-
@SmallTest
@RunWith(LauncherMultivalentJUnit.class)
public class SystemShortcutTest {
@@ -86,7 +86,7 @@
private TestSandboxModelContextWrapper mTestContext;
private final SandboxModelContext mSandboxContext = new SandboxModelContext();
private PrivateProfileManager mPrivateProfileManager;
- private PopupDataProvider mPopupDataProvider;
+ private WidgetPickerDataProvider mWidgetPickerDataProvider;
private AppInfo mAppInfo;
@Mock UserCache mUserCache;
@Mock ApiWrapper mApiWrapper;
@@ -119,8 +119,8 @@
spyOn(mPrivateProfileManager);
when(mPrivateProfileManager.getProfileUser()).thenReturn(PRIVATE_HANDLE);
- mPopupDataProvider = mTestContext.getPopupDataProvider();
- spyOn(mPopupDataProvider);
+ mWidgetPickerDataProvider = mTestContext.getWidgetPickerDataProvider();
+ spyOn(mWidgetPickerDataProvider);
}
@After
@@ -141,7 +141,7 @@
mAppInfo = new AppInfo();
mAppInfo.componentName = new ComponentName(mTestContext, getClass());
assertNotNull(mAppInfo.getTargetComponent());
- doReturn(new ArrayList<>()).when(mPopupDataProvider).getWidgetsForPackageUser(any());
+ doReturn(new WidgetPickerData()).when(mWidgetPickerDataProvider).get();
spyOn(mAppInfo);
SystemShortcut systemShortcut = SystemShortcut.WIDGETS
.getShortcut(mTestContext, mAppInfo, mView);