diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java index a202095c47..cc5e1cbb46 100644 --- a/go/src/com/android/launcher3/model/WidgetsModel.java +++ b/go/src/com/android/launcher3/model/WidgetsModel.java @@ -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 getWidgetsList(Context context) { + public synchronized ArrayList getWidgetsListForPicker(Context context) { return EMPTY_WIDGET_LIST; } + /** Returns a mapping of packages to their widgets without static shortcuts. */ + public synchronized Map> 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. diff --git a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java new file mode 100644 index 0000000000..70a143ee6f --- /dev/null +++ b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java @@ -0,0 +1,263 @@ +/* + * 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 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 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 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 addNotAnimated, + ArrayList addAnimated) { } + + @Override + public void bindIncrementalDownloadProgressUpdated(AppInfo app) { } + + @Override + public void bindWorkspaceItemsChanged(List updated) { } + + @Override + public void bindWidgetsRestored(ArrayList widgets) { } + + @Override + public void bindRestoreItemsChange(HashSet updates) { } + + @Override + public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { } + + @Override + public void bindAllWidgets(List widgets) { } + + @Override + public void onPageBoundSynchronously(int page) { } + + @Override + public void executeOnNextDraw(ViewOnDrawExecutor executor) { } + + @Override + public void bindDeepShortcutMap(HashMap deepShortcutMap) { } + + @Override + public void bindAllApplications(AppInfo[] apps, int flags) { } + } +} diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java index 2ea8bd20f6..b1b4d70152 100644 --- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java +++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java @@ -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) { diff --git a/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java new file mode 100644 index 0000000000..a29ac1a7de --- /dev/null +++ b/quickstep/src/com/android/launcher3/model/WidgetsPredictionUpdateTask.java @@ -0,0 +1,82 @@ +/* + * 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 mTargets; + + WidgetsPredictionUpdateTask(PredictorState predictorState, List targets) { + mPredictorState = predictorState; + mTargets = targets; + } + + /** + * Uses the app predication result to infer widgets that the user may want to use. + * + *

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 widgetsInWorkspace = dataModel.appWidgets.stream().map( + widget -> new ComponentKey(widget.providerName, widget.user)).collect( + Collectors.toSet()); + Map> allWidgets = + dataModel.widgetsModel.getAllWidgetsWithoutShortcuts(); + + ArrayList recommendedWidgetsInDescendingOrder = new ArrayList<>(); + for (AppTarget app : mTargets) { + PackageUserKey packageUserKey = new PackageUserKey(app.getPackageName(), app.getUser()); + if (allWidgets.containsKey(packageUserKey)) { + List 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. + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 55dde4569d..b009629c80 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -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); } } diff --git a/robolectric_tests/resources/widgets_predication_update_task_data.txt b/robolectric_tests/resources/widgets_predication_update_task_data.txt new file mode 100644 index 0000000000..941d1954e2 --- /dev/null +++ b/robolectric_tests/resources/widgets_predication_update_task_data.txt @@ -0,0 +1,24 @@ +# Model data used by WidgetsPredictionUpdateTasksTest + +classMap s com.android.launcher3.model.data.WorkspaceItemInfo +classMap w com.android.launcher3.model.data.LauncherAppWidgetInfo + +# Items for the BgDataModel + +# App shortcuts +bgItem s itemType=0 title=app1-class1 intent=component=app1/class1 id=1 +bgItem s itemType=0 title=app1-class2 intent=component=app1/class2 id=2 +bgItem s itemType=0 title=app2-class1 intent=component=app2/class1 id=3 +bgItem s itemType=0 title=app2-class2 intent=component=app2/class2 id=4 + +# Promise icons for app3 +bgItem s itemType=0 status=2 title=app3-class1 intent=component=app3/class1 id=5 +bgItem s itemType=0 status=2 title=app3-class2 intent=component=app3/class2 id=6 +bgItem s itemType=1 status=1 title=app3-shrt intent=component=app3/class3 id=7 + +# Promise icon for app4 +bgItem s itemType=1 status=1 title=app4-shrt intent=component=app4/class1 id=8 + +# Widget +bgItem w providerName=app4/provider1 id=9 +bgItem w providerName=app5/provider1 id=10 \ No newline at end of file diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java index dfdc53c1c9..22c257a8cb 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -193,6 +193,7 @@ public class LauncherSettings { public static final int CONTAINER_DESKTOP = -100; public static final int CONTAINER_HOTSEAT = -101; public static final int CONTAINER_PREDICTION = -102; + public static final int CONTAINER_WIDGETS_PREDICTION = -111; public static final int CONTAINER_HOTSEAT_PREDICTION = -103; public static final int CONTAINER_ALL_APPS = -104; public static final int CONTAINER_WIDGETS_TRAY = -105; diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java index 543ee2adf7..ad553d5e46 100644 --- a/src/com/android/launcher3/model/BaseModelUpdateTask.java +++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java @@ -124,7 +124,7 @@ public abstract class BaseModelUpdateTask implements ModelUpdateTask { public void bindUpdatedWidgets(BgDataModel dataModel) { final ArrayList widgets = - dataModel.widgetsModel.getWidgetsList(mApp.getContext()); + dataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext()); scheduleCallbackTask(c -> c.bindAllWidgets(widgets)); } diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index 18536325eb..778089485a 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -58,8 +58,11 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan private HashMap mDeepShortcutMap = new HashMap<>(); /** Maps packages to their DotInfo's . */ private Map mPackageUserToDotInfos = new HashMap<>(); - /** Maps packages to their Widgets */ + + /** All installed widgets. */ private List mAllWidgets = List.of(); + /** Widgets that can be recommended to the users. */ + private List mRecommendedWidgets = List.of(); private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE; @@ -187,6 +190,15 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan notificationListener.cancelNotificationFromLauncher(notificationKey); } + /** + * Sets a list of recommended widgets ordered by their order of appearance in the widgets + * recommendation UI. + */ + public void setRecommendedWidgets(List recommendedWidgets) { + mRecommendedWidgets = recommendedWidgets; + mChangeListener.onRecommendedWidgetsBound(); + } + public void setAllWidgets(List allWidgets) { mAllWidgets = allWidgets; mChangeListener.onWidgetsBound(); @@ -200,6 +212,21 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan return mAllWidgets; } + /** Returns a list of recommended widgets. */ + public List getRecommendedWidgets() { + HashMap 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))) + .collect(Collectors.toList()); + } + public List getWidgetsForPackageUser(PackageUserKey packageUserKey) { return mAllWidgets.stream() .filter(row -> row instanceof WidgetsListContentEntry @@ -244,5 +271,8 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan default void trimNotifications(Map updatedDots) { } default void onWidgetsBound() { } + + /** A callback to get notified when recommended widgets are bound. */ + default void onRecommendedWidgetsBound() { } } } diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java index 73b1601c34..abce2a24f4 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java @@ -48,7 +48,7 @@ public class LoaderResults extends BaseLoaderResults { @Override public void bindWidgets() { final List widgets = - mBgDataModel.widgetsModel.getWidgetsList(mApp.getContext()); + mBgDataModel.widgetsModel.getWidgetsListForPicker(mApp.getContext()); executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor); } } diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java index a8c7c4825b..3ea47660b0 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java @@ -5,6 +5,8 @@ import static android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_HIDE_FROM_P import static com.android.launcher3.pm.ShortcutConfigActivityInfo.queryList; +import static java.util.stream.Collectors.toList; + import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; @@ -67,7 +69,7 @@ public class WidgetsModel { * * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) */ - public synchronized ArrayList getWidgetsList(Context context) { + public synchronized ArrayList getWidgetsListForPicker(Context context) { ArrayList result = new ArrayList<>(); AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context); @@ -82,6 +84,22 @@ public class WidgetsModel { return result; } + /** Returns a mapping of packages to their widgets without static shortcuts. */ + public synchronized Map> getAllWidgetsWithoutShortcuts() { + Map> packagesToWidgets = new HashMap<>(); + mWidgetsList.forEach((packageItemInfo, widgetsAndShortcuts) -> { + List widgets = widgetsAndShortcuts.stream() + .filter(item -> item.widgetInfo != null) + .collect(toList()); + if (widgets.size() > 0) { + packagesToWidgets.put( + new PackageUserKey(packageItemInfo.packageName, packageItemInfo.user), + widgets); + } + }); + return packagesToWidgets; + } + /** * @param packageUser If null, all widgets and shortcuts are updated and returned, otherwise * only widgets and shortcuts associated with the package/user are.