Merge "Update prediction task to provide categorized suggestions." into main

This commit is contained in:
Shamali Patwa
2024-02-06 21:36:14 +00:00
committed by Android (Google) Code Review
5 changed files with 105 additions and 25 deletions
@@ -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<ItemInfo> items = servicePredictedItems.stream()
.map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION))
.collect(Collectors.toList());
List<ItemInfo> 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);
@@ -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<PendingAddWidgetInfo> 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<AppTarget> appTargets) {
return new WidgetsPredictionUpdateTask(
return new WidgetsPredictionUpdateTask(
new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"),
appTargets);
}
@@ -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<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)
.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
@@ -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;
@@ -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);
}