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

Commit 3a835325 authored by Steven Ng's avatar Steven Ng
Browse files

Widgets recommendation backend

Add a widgets recommendation mechanism based on AiAI app predication
ranking with the following changes:

1. Only one widget is picked from one app.
2. Widgets that are already added to the workspace are excluded from
   the recommendation.

Test: run PredicationUpdateTaskTest

Bug: 179797520
Change-Id: Ia697bc6df0bae75969e68b7b3de32d57901f7461
parent 1f4a3414
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
@@ -53,10 +54,15 @@ public class WidgetsModel {
     *
     * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List)
     */
    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsList(Context context) {
    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsListForPicker(Context context) {
        return EMPTY_WIDGET_LIST;
    }

    /** Returns a mapping of packages to their widgets without static shortcuts. */
    public synchronized Map<PackageUserKey, List<WidgetItem>> getAllWidgetsWithoutShortcuts() {
        return Map.of();
    }

    /**
     * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise
     *                    only widgets and shortcuts associated with the package/user are.
+263 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.model;

import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.robolectric.Shadows.shadowOf;

import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetId;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.os.Process;
import android.os.UserHandle;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.icons.ComponentWithLabel;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.LauncherModelHelper;
import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowAppWidgetManager;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;

@RunWith(RobolectricTestRunner.class)
public final class WidgetsPredicationUpdateTaskTest {

    private AppWidgetProviderInfo mApp1Provider1 = new AppWidgetProviderInfo();
    private AppWidgetProviderInfo mApp1Provider2 = new AppWidgetProviderInfo();
    private AppWidgetProviderInfo mApp2Provider1 = new AppWidgetProviderInfo();
    private AppWidgetProviderInfo mApp4Provider1 = new AppWidgetProviderInfo();
    private AppWidgetProviderInfo mApp4Provider2 = new AppWidgetProviderInfo();
    private AppWidgetProviderInfo mApp5Provider1 = new AppWidgetProviderInfo();

    private FakeBgDataModelCallback mCallback = new FakeBgDataModelCallback();
    private Context mContext;
    private LauncherModelHelper mModelHelper;
    private UserHandle mUserHandle;
    private InvariantDeviceProfile mTestProfile;

    @Mock
    private IconCache mIconCache;

    @Before
    public void setup() throws Exception {
        MockitoAnnotations.initMocks(this);
        doAnswer(invocation -> {
            ComponentWithLabel componentWithLabel = invocation.getArgument(0);
            return componentWithLabel.getComponent().getShortClassName();
        }).when(mIconCache).getTitleNoCache(any());

        mContext = RuntimeEnvironment.application;
        mModelHelper = new LauncherModelHelper();
        mUserHandle = Process.myUserHandle();
        mTestProfile = new InvariantDeviceProfile();
        // 2 widgets, app4/provider1 & app5/provider1, have already been added to the workspace.
        mModelHelper.initializeData("/widgets_predication_update_task_data.txt");

        ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager());
        mApp1Provider1.provider = ComponentName.createRelative("app1", "provider1");
        ReflectionHelpers.setField(mApp1Provider1, "providerInfo",
                packageManager.addReceiverIfNotPresent(mApp1Provider1.provider));
        mApp1Provider2.provider = ComponentName.createRelative("app1", "provider2");
        ReflectionHelpers.setField(mApp1Provider2, "providerInfo",
                packageManager.addReceiverIfNotPresent(mApp1Provider2.provider));
        mApp2Provider1.provider = ComponentName.createRelative("app2", "provider1");
        ReflectionHelpers.setField(mApp2Provider1, "providerInfo",
                packageManager.addReceiverIfNotPresent(mApp2Provider1.provider));
        mApp4Provider1.provider = ComponentName.createRelative("app4", "provider1");
        ReflectionHelpers.setField(mApp4Provider1, "providerInfo",
                packageManager.addReceiverIfNotPresent(mApp4Provider1.provider));
        mApp4Provider2.provider = ComponentName.createRelative("app4", ".provider2");
        ReflectionHelpers.setField(mApp4Provider2, "providerInfo",
                packageManager.addReceiverIfNotPresent(mApp4Provider2.provider));
        mApp5Provider1.provider = ComponentName.createRelative("app5", "provider1");
        ReflectionHelpers.setField(mApp5Provider1, "providerInfo",
                packageManager.addReceiverIfNotPresent(mApp5Provider1.provider));

        ShadowAppWidgetManager shadowAppWidgetManager =
                shadowOf(mContext.getSystemService(AppWidgetManager.class));
        shadowAppWidgetManager.addInstalledProvider(mApp1Provider1);
        shadowAppWidgetManager.addInstalledProvider(mApp1Provider2);
        shadowAppWidgetManager.addInstalledProvider(mApp2Provider1);
        shadowAppWidgetManager.addInstalledProvider(mApp4Provider1);
        shadowAppWidgetManager.addInstalledProvider(mApp4Provider2);
        shadowAppWidgetManager.addInstalledProvider(mApp5Provider1);

        mModelHelper.getModel().addCallbacks(mCallback);

        MODEL_EXECUTOR.post(() -> mModelHelper.getBgDataModel().widgetsModel.update(
                LauncherAppState.getInstance(mContext), /* packageUser= */ null));
        waitUntilIdle();
    }


    @Test
    public void widgetsRecommendationRan_shouldOnlyReturnNotAddedWidgetsInAppPredictionOrder()
            throws Exception {
        // WHEN newPredicationTask is executed with app predication of 5 apps.
        AppTarget app1 = new AppTarget(new AppTargetId("app1"), "app1", "className",
                mUserHandle);
        AppTarget app2 = new AppTarget(new AppTargetId("app2"), "app2", "className",
                mUserHandle);
        AppTarget app3 = new AppTarget(new AppTargetId("app3"), "app3", "className",
                mUserHandle);
        AppTarget app4 = new AppTarget(new AppTargetId("app4"), "app4", "className",
                mUserHandle);
        AppTarget app5 = new AppTarget(new AppTargetId("app5"), "app5", "className",
                mUserHandle);
        mModelHelper.executeTaskForTest(
                newWidgetsPredicationTask(List.of(app5, app3, app2, app4, app1)))
                .forEach(Runnable::run);

        // THEN only 3 widgets are returned because
        // 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.
        List<PendingAddWidgetInfo> recommendedWidgets = mCallback.mRecommendedWidgets.items
                .stream()
                .map(itemInfo -> (PendingAddWidgetInfo) itemInfo)
                .collect(Collectors.toList());
        assertThat(recommendedWidgets).hasSize(3);
        assertWidgetInfo(recommendedWidgets.get(0).info, mApp2Provider1);
        assertWidgetInfo(recommendedWidgets.get(1).info, mApp4Provider2);
        assertWidgetInfo(recommendedWidgets.get(2).info, mApp1Provider1);
    }

    private void assertWidgetInfo(
            LauncherAppWidgetProviderInfo actual, AppWidgetProviderInfo expected) {
        assertThat(actual.provider).isEqualTo(expected.provider);
        assertThat(actual.getUser()).isEqualTo(expected.getProfile());
    }

    private void waitUntilIdle() {
        shadowOf(MODEL_EXECUTOR.getLooper()).idle();
        shadowOf(MAIN_EXECUTOR.getLooper()).idle();
    }

    private WidgetsPredictionUpdateTask newWidgetsPredicationTask(List<AppTarget> appTargets) {
        return new WidgetsPredictionUpdateTask(
                new PredictorState(CONTAINER_WIDGETS_PREDICTION, "test_widgets_prediction"),
                appTargets);
    }

    private final class FakeBgDataModelCallback implements BgDataModel.Callbacks {

        private FixedContainerItems mRecommendedWidgets = null;

        @Override
        public void bindExtraContainerItems(FixedContainerItems item) {
            mRecommendedWidgets = item;
        }

        @Override
        public int getPageToBindSynchronously() {
            return 0;
        }

        @Override
        public void clearPendingBinds() { }

        @Override
        public void startBinding() { }

        @Override
        public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }

        @Override
        public void bindScreens(IntArray orderedScreenIds) { }

        @Override
        public void finishFirstPageBind(ViewOnDrawExecutor executor) { }

        @Override
        public void finishBindingItems(int pageBoundFirst) { }

        @Override
        public void preAddApps() { }

        @Override
        public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
                ArrayList<ItemInfo> addAnimated) { }

        @Override
        public void bindIncrementalDownloadProgressUpdated(AppInfo app) { }

        @Override
        public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }

        @Override
        public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }

        @Override
        public void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }

        @Override
        public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }

        @Override
        public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }

        @Override
        public void onPageBoundSynchronously(int page) { }

        @Override
        public void executeOnNextDraw(ViewOnDrawExecutor executor) { }

        @Override
        public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }

        @Override
        public void bindAllApplications(AppInfo[] apps, int flags) { }
    }
}
+30 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static android.text.format.DateUtils.formatElapsedTime;
import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
import static com.android.launcher3.Utilities.getDevicePrefs;
@@ -80,6 +81,8 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange
            new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions");
    private final PredictorState mHotseatState =
            new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions");
    private final PredictorState mWidgetsRecommendationState =
            new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction");

    private final InvariantDeviceProfile mIDP;
    private final AppEventProducer mAppEventProducer;
@@ -111,6 +114,9 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange
        mHotseatState.items.setItems(
                mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get));
        mDataModel.extraItems.put(CONTAINER_HOTSEAT_PREDICTION, mHotseatState.items);

        // Widgets prediction isn't used frequently. And thus, it is not persisted on disk.
        mDataModel.extraItems.put(CONTAINER_WIDGETS_PREDICTION, mWidgetsRecommendationState.items);
        mActive = true;
    }

@@ -161,6 +167,9 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange
        if (mAllAppsState.predictor != null) {
            mAllAppsState.predictor.requestPredictionUpdate();
        }
        if (mWidgetsRecommendationState.predictor != null) {
            mWidgetsRecommendationState.predictor.requestPredictionUpdate();
        }
    }

    @Override
@@ -176,6 +185,7 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange
    private void destroyPredictors() {
        mAllAppsState.destroyPredictor();
        mHotseatState.destroyPredictor();
        mWidgetsRecommendationState.destroyPredictor();
    }

    @WorkerThread
@@ -203,6 +213,12 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange
                        .setPredictedTargetCount(mIDP.numHotseatIcons)
                        .setExtras(convertDataModelToAppTargetBundle(context, mDataModel))
                        .build()));

        registerWidgetsPredictor(apm.createAppPredictionSession(
                new AppPredictionContext.Builder(context)
                        .setUiSurface("widgets")
                        .setPredictedTargetCount(mIDP.numColumns)
                        .build()));
    }

    private void registerPredictor(PredictorState state, AppPredictor predictor) {
@@ -220,6 +236,20 @@ public class QuickstepModelDelegate extends ModelDelegate implements OnIDPChange
        mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets));
    }

    private void registerWidgetsPredictor(AppPredictor predictor) {
        mWidgetsRecommendationState.predictor = predictor;
        mWidgetsRecommendationState.predictor.registerPredictionUpdates(
                Executors.MODEL_EXECUTOR, targets -> {
                    if (mWidgetsRecommendationState.setTargets(targets)) {
                        // No diff, skip
                        return;
                    }
                    mApp.getModel().enqueueModelUpdateTask(
                            new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets));
                });
        mWidgetsRecommendationState.predictor.requestPredictionUpdate();
    }

    @Override
    public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) {
        if ((changeFlags & CHANGE_FLAG_GRID) != 0) {
+82 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.model;

import android.app.prediction.AppTarget;

import com.android.launcher3.LauncherAppState;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
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 java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/** Task to update model as a result of predicted widgets update */
public final class WidgetsPredictionUpdateTask extends BaseModelUpdateTask {
    private final PredictorState mPredictorState;
    private final List<AppTarget> mTargets;

    WidgetsPredictionUpdateTask(PredictorState predictorState, List<AppTarget> targets) {
        mPredictorState = predictorState;
        mTargets = targets;
    }

    /**
     * Uses the app predication result to infer widgets that the user may want to use.
     *
     * <p>The algorithm uses the app prediction ranking to create a widgets ranking which only
     * includes one widget per app and excludes widgets that have already been added to the
     * workspace.
     */
    @Override
    public void execute(LauncherAppState appState, BgDataModel dataModel, AllAppsList apps) {
        Set<ComponentKey> widgetsInWorkspace = dataModel.appWidgets.stream().map(
                widget -> new ComponentKey(widget.providerName, widget.user)).collect(
                Collectors.toSet());
        Map<PackageUserKey, List<WidgetItem>> allWidgets =
                dataModel.widgetsModel.getAllWidgetsWithoutShortcuts();

        ArrayList<ItemInfo> recommendedWidgetsInDescendingOrder = new ArrayList<>();
        for (AppTarget app : mTargets) {
            PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), app.getUser());
            if (allWidgets.containsKey(packageUserKey)) {
                List<WidgetItem> notAddedWidgets = allWidgets.get(packageUserKey).stream()
                        .filter(item ->
                                !widgetsInWorkspace.contains(
                                        new ComponentKey(item.componentName, item.user)))
                        .collect(Collectors.toList());
                if (notAddedWidgets.size() > 0) {
                    // Even an apps have more than one widgets, we only include one widget.
                    recommendedWidgetsInDescendingOrder.add(
                            new PendingAddWidgetInfo(notAddedWidgets.get(0).widgetInfo));
                }
            }
        }
        FixedContainerItems fixedContainerItems = mPredictorState.items;
        fixedContainerItems.items.clear();
        fixedContainerItems.items.addAll(recommendedWidgetsInDescendingOrder);
        bindExtraContainerItems(fixedContainerItems);

        // Don't store widgets prediction to disk because it is not used frequently.
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -218,6 +218,8 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
                    .setPredictedApps(item.items);
        } else if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
            mHotseatPredictionController.setPredictedItems(item);
        } else if (item.containerId == Favorites.CONTAINER_WIDGETS_PREDICTION) {
            getPopupDataProvider().setRecommendedWidgets(item.items);
        }
    }

Loading