diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java index 6160378767..f4cbf17df6 100644 --- a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -15,9 +15,11 @@ */ package com.android.launcher3.model; +import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions; import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; import android.app.prediction.AppTarget; +import android.content.Context; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -29,6 +31,7 @@ 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.picker.WidgetRecommendationCategoryProvider; import java.util.ArrayList; import java.util.List; @@ -93,9 +96,21 @@ public final class WidgetsPredictionUpdateTask extends BaseModelUpdateTask { servicePredictedItems.addAll(localFilteredWidgets); } - List items = servicePredictedItems.stream() - .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION)) - .collect(Collectors.toList()); + List items; + if (enableCategorizedWidgetSuggestions()) { + Context context = appState.getContext(); + WidgetRecommendationCategoryProvider categoryProvider = + WidgetRecommendationCategoryProvider.newInstance(context); + items = servicePredictedItems.stream() + .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, + categoryProvider.getWidgetRecommendationCategory(context, it))) + .collect(Collectors.toList()); + } else { + items = servicePredictedItems.stream() + .map(it -> new PendingAddWidgetInfo(it.widgetInfo, + CONTAINER_WIDGETS_PREDICTION)).collect( + Collectors.toList()); + } FixedContainerItems fixedContainerItems = new FixedContainerItems(mPredictorState.containerId, items); diff --git a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java index b12d98b8ce..37dde10d0e 100644 --- a/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java +++ b/quickstep/tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -26,6 +26,8 @@ import static com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; @@ -36,11 +38,14 @@ import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.os.UserHandle; +import android.platform.test.flag.junit.SetFlagsRule; import android.text.TextUtils; +import androidx.test.core.content.pm.ApplicationInfoBuilder; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.Flags; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.QuickstepModelDelegate.PredictorState; import com.android.launcher3.util.LauncherLayoutBuilder; @@ -50,6 +55,7 @@ import com.android.launcher3.widget.PendingAddWidgetInfo; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,6 +67,9 @@ import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public final class WidgetsPredicationUpdateTaskTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private AppWidgetProviderInfo mApp1Provider1; private AppWidgetProviderInfo mApp1Provider2; private AppWidgetProviderInfo mApp2Provider1; @@ -75,6 +84,7 @@ public final class WidgetsPredicationUpdateTaskTest { @Before public void setup() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CATEGORIZED_WIDGET_SUGGESTIONS); mModelHelper = new LauncherModelHelper(); mUserHandle = myUserHandle(); @@ -93,6 +103,12 @@ public final class WidgetsPredicationUpdateTaskTest { allWidgets = Arrays.asList(mApp1Provider1, mApp1Provider2, mApp2Provider1, mApp4Provider1, mApp4Provider2, mApp5Provider1); + doAnswer(i -> { + String pkg = i.getArgument(0); + return ApplicationInfoBuilder.newBuilder().setPackageName(pkg).setName( + "App " + pkg).build(); + }).when(mModelHelper.sandboxContext.getPackageManager()) + .getApplicationInfo(anyString(), anyInt()); AppWidgetManager manager = mModelHelper.sandboxContext.spyService(AppWidgetManager.class); doReturn(allWidgets).when(manager).getInstalledProviders(); doReturn(allWidgets).when(manager).getInstalledProvidersForProfile(eq(myUserHandle())); @@ -140,12 +156,16 @@ public final class WidgetsPredicationUpdateTaskTest { // 1. app5/provider1 & app4/provider1 have already been added to workspace. They are // excluded from the result. // 2. app3 doesn't have a widget. - // 3. only 1 widget is picked from app1 because we only want to promote one widget per app. + // 3. only 1 widget is picked from app1 because we only want to promote one widget + // per app. List recommendedWidgets = mCallback.mRecommendedWidgets.items .stream() .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); assertThat(recommendedWidgets).hasSize(2); + recommendedWidgets.forEach(pendingAddWidgetInfo -> + assertThat(pendingAddWidgetInfo.recommendationCategory).isNotNull() + ); assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1); assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1); }); @@ -179,6 +199,9 @@ public final class WidgetsPredicationUpdateTaskTest { .map(itemInfo -> (PendingAddWidgetInfo) itemInfo) .collect(Collectors.toList()); assertThat(recommendedWidgets).hasSize(2); + recommendedWidgets.forEach(pendingAddWidgetInfo -> + assertThat(pendingAddWidgetInfo.recommendationCategory).isNotNull() + ); // Another widget from the same package assertWidgetInfo(recommendedWidgets.get(0).info, mApp4Provider2); assertWidgetInfo(recommendedWidgets.get(1).info, mApp1Provider1); @@ -192,7 +215,7 @@ public final class WidgetsPredicationUpdateTaskTest { } private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List appTargets) { - return new WidgetsPredictionUpdateTask( + return new WidgetsPredictionUpdateTask( new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"), appTargets); } diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index 5f179593fd..f1d837c7b4 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -31,8 +31,10 @@ 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; @@ -41,6 +43,7 @@ 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; @@ -218,6 +221,32 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan .collect(Collectors.toList()); } + /** Returns the recommended widgets mapped by their category. */ + public Map> getCategorizedRecommendedWidgets() { + Map 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) + .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 getWidgetsForPackageUser(PackageUserKey packageUserKey) { return mAllWidgets.stream() .filter(row -> row instanceof WidgetsListContentEntry diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java index ccf4b2ea04..a501960920 100644 --- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java +++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java @@ -27,6 +27,7 @@ import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.model.data.FolderInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; +import com.android.launcher3.widget.picker.WidgetRecommendationCategory; import com.android.launcher3.widget.util.WidgetSizes; /** @@ -42,6 +43,16 @@ public class PendingAddWidgetInfo extends PendingAddItemInfo { public Bundle bindOptions = null; public int sourceContainer; + public WidgetRecommendationCategory recommendationCategory = null; + + public PendingAddWidgetInfo( + LauncherAppWidgetProviderInfo i, + int container, + WidgetRecommendationCategory recommendationCategory) { + this(i, container); + this.recommendationCategory = recommendationCategory; + } + public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, int container) { if (i.isCustomWidget()) { itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java index d99c5d1e6d..801b1f662f 100644 --- a/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java +++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationCategoryProvider.java @@ -80,6 +80,28 @@ public class WidgetRecommendationCategoryProvider implements ResourceBasedOverri /** Maps application category to an appropriate displayable category. */ private static WidgetRecommendationCategory getCategoryFromApplicationCategory( Context context, int applicationCategory, String componentName) { + // Weather categories don't map to a specific application category, so, we maintain an + // allowlist. + String[] weatherRecommendationAllowlist = + context.getResources().getStringArray(R.array.weather_recommendations); + for (String allowedWeatherComponentName : weatherRecommendationAllowlist) { + if (componentName.equalsIgnoreCase(allowedWeatherComponentName)) { + return new WidgetRecommendationCategory( + R.string.weather_widget_recommendation_category_label, /*order=*/3); + } + } + + // Fitness categories don't map to a specific application category, so, we maintain an + // allowlist. + String[] fitnessRecommendationAllowlist = + context.getResources().getStringArray(R.array.fitness_recommendations); + for (String allowedFitnessComponentName : fitnessRecommendationAllowlist) { + if (componentName.equalsIgnoreCase(allowedFitnessComponentName)) { + return new WidgetRecommendationCategory( + R.string.fitness_widget_recommendation_category_label, /*order=*/2); + } + } + if (applicationCategory == ApplicationInfo.CATEGORY_PRODUCTIVITY) { return new WidgetRecommendationCategory( R.string.productivity_widget_recommendation_category_label, /*order=*/0); @@ -99,26 +121,6 @@ public class WidgetRecommendationCategoryProvider implements ResourceBasedOverri /*order=*/4); } - // Fitness & weather categories don't map to a specific application category, so, we - // maintain an allowlist. - String[] weatherRecommendationAllowlist = - context.getResources().getStringArray(R.array.weather_recommendations); - for (String allowedWeatherComponentName : weatherRecommendationAllowlist) { - if (componentName.equalsIgnoreCase(allowedWeatherComponentName)) { - return new WidgetRecommendationCategory( - R.string.weather_widget_recommendation_category_label, /*order=*/2); - } - } - - String[] fitnessRecommendationAllowlist = - context.getResources().getStringArray(R.array.fitness_recommendations); - for (String allowedFitnessComponentName : fitnessRecommendationAllowlist) { - if (componentName.equalsIgnoreCase(allowedFitnessComponentName)) { - return new WidgetRecommendationCategory( - R.string.fitness_widget_recommendation_category_label, /*order=*/3); - } - } - return new WidgetRecommendationCategory( R.string.others_widget_recommendation_category_label, /*order=*/5); }