Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 57c1eedb authored by Shamali P's avatar Shamali P
Browse files

Update prediction task to provide categorized suggestions.

Also adds a helper in popup data provider to provide categorized
suggestions. Used in follow up CL to update the UI.

Bug: 318410881
Test: WidgetsPredictionUpdateTaskTest and manual with follow up changes
Flag: ACONFIG com.android.launcher3.enable_categorized_widget_recommendations DEVELOPMENT
Change-Id: Ie80e8ba7bbe874f7c4b0e579446edf571036555e
parent f9eced57
Loading
Loading
Loading
Loading
+18 −3
Original line number Diff line number Diff line
@@ -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))
        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);

+25 −2
Original line number Diff line number Diff line
@@ -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);
+29 −0
Original line number Diff line number Diff line
@@ -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
+11 −0
Original line number Diff line number Diff line
@@ -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;
+22 −20
Original line number Diff line number Diff line
@@ -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);
    }