Loading quickstep/src/com/android/launcher3/WidgetPickerActivity.java +75 −8 Original line number Diff line number Diff line Loading @@ -35,23 +35,31 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.dragndrop.SimpleDragLayer; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.WidgetPredictionsRequester; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.BaseWidgetSheet; import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.picker.WidgetsFullSheet; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; /** An Activity that can host Launcher's widget picker. */ public class WidgetPickerActivity extends BaseActivity { private static final String TAG = "WidgetPickerActivity"; /** * Name of the extra that indicates that a widget being dragged. * Loading @@ -64,14 +72,33 @@ public class WidgetPickerActivity extends BaseActivity { // the intent, then widgets will not be filtered for size. private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"; private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"; /** * Widgets currently added by the user in the UI surface. * <p>This allows widget picker to exclude existing widgets from suggestions.</p> */ private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets"; /** * A unique identifier of the surface hosting the widgets; * <p>"widgets" is reserved for home screen surface.</p> * <p>"widgets_hub" is reserved for glanceable hub surface.</p> */ private static final String EXTRA_UI_SURFACE = "ui_surface"; private static final Pattern UI_SURFACE_PATTERN = Pattern.compile("^(widgets|widgets_hub)$"); private SimpleDragLayer<WidgetPickerActivity> mDragLayer; private WidgetsModel mModel; private LauncherAppState mApp; private WidgetPredictionsRequester mWidgetPredictionsRequester; private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {}); private int mDesiredWidgetWidth; private int mDesiredWidgetHeight; private int mWidgetCategoryFilter; @Nullable private String mUiSurface; // Widgets existing on the host surface. @NonNull private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { Loading @@ -80,9 +107,8 @@ public class WidgetPickerActivity extends BaseActivity { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); LauncherAppState app = LauncherAppState.getInstance(this); InvariantDeviceProfile idp = app.getInvariantDeviceProfile(); mApp = LauncherAppState.getInstance(this); InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); mDeviceProfile = idp.getDeviceProfile(this); mModel = new WidgetsModel(); Loading @@ -97,6 +123,11 @@ public class WidgetPickerActivity extends BaseActivity { widgetSheet.disableNavBarScrim(true); widgetSheet.addOnCloseListener(this::finish); parseIntentExtras(); refreshAndBindWidgets(); } private void parseIntentExtras() { // A value of 0 for either size means that no filtering will occur in that dimension. If // both values are 0, then no size filtering will occur. mDesiredWidgetWidth = Loading @@ -108,7 +139,15 @@ public class WidgetPickerActivity extends BaseActivity { mWidgetCategoryFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0); refreshAndBindWidgets(); String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE); if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) { mUiSurface = uiSurfaceParam; } ArrayList<AppWidgetProviderInfo> addedWidgets = getIntent().getParcelableArrayListExtra( EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class); if (addedWidgets != null) { mAddedWidgets = addedWidgets; } } @NonNull Loading Loading @@ -179,11 +218,12 @@ public class WidgetPickerActivity extends BaseActivity { }; } /** Updates the model with widgets and provides them after applying the provided filter. */ private void refreshAndBindWidgets() { MODEL_EXECUTOR.execute(() -> { LauncherAppState app = LauncherAppState.getInstance(this); mModel.update(app, null); final ArrayList<WidgetsListBaseEntry> widgets = final List<WidgetsListBaseEntry> allWidgets = mModel.getFilteredWidgetsListForPicker( app.getContext(), /*widgetItemFilter=*/ widget -> { Loading @@ -193,10 +233,37 @@ public class WidgetPickerActivity extends BaseActivity { return verdict.isAcceptable; } ); MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); bindWidgets(allWidgets); if (mUiSurface != null) { Map<PackageUserKey, List<WidgetItem>> allWidgetsMap = allWidgets.stream() .filter(WidgetsListHeaderEntry.class::isInstance) .collect(Collectors.toMap( entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem), entry -> entry.mWidgets) ); mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(), mUiSurface, allWidgetsMap); mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); } }); } private void bindWidgets(List<WidgetsListBaseEntry> widgets) { MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); } private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) { MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets)); } @Override protected void onDestroy() { super.onDestroy(); if (mWidgetPredictionsRequester != null) { mWidgetPredictionsRequester.clear(); } } private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) { final AppWidgetProviderInfo info = widget.widgetInfo; if (info == null) { Loading quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java 0 → 100644 +233 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.Flags.enableCategorizedWidgetSuggestions; 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 android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionManager; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; 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.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; /** * Works with app predictor to fetch and process widget predictions displayed in a standalone * widget picker activity for a UI surface. */ public class WidgetPredictionsRequester { private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; @Nullable private AppPredictor mAppPredictor; private final Context mContext; @NonNull private final String mUiSurface; @NonNull private final Map<PackageUserKey, List<WidgetItem>> mAllWidgets; public WidgetPredictionsRequester(Context context, @NonNull String uiSurface, @NonNull Map<PackageUserKey, List<WidgetItem>> allWidgets) { mContext = context; mUiSurface = uiSurface; mAllWidgets = Collections.unmodifiableMap(allWidgets); } /** * Requests predictions from the app predictions manager and registers the provided callback to * receive updates when predictions are available. * * @param existingWidgets widgets that are currently added to the surface; * @param callback consumer of prediction results to be called when predictions are * available */ public void request(List<AppWidgetProviderInfo> existingWidgets, Consumer<List<ItemInfo>> callback) { Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface); Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets); MODEL_EXECUTOR.execute(() -> { clear(); AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class); if (apm == null) { return; } mAppPredictor = apm.createAppPredictionSession( new AppPredictionContext.Builder(mContext) .setUiSurface(mUiSurface) .setExtras(bundle) .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) .build()); mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, targets -> bindPredictions(targets, filter, callback)); mAppPredictor.requestPredictionUpdate(); }); } /** * Returns a bundle that can be passed in a prediction session * * @param addedWidgets widgets that are already added by the user in the ui surface * @param uiSurface a unique identifier of the surface hosting widgets; format * "widgets_xx"; note - "widgets" is reserved for home screen surface. */ @VisibleForTesting static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets, String uiSurface) { Bundle bundle = new Bundle(); ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>(); for (AppWidgetProviderInfo info : addedWidgets) { ComponentName componentName = info.provider; AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName); addedAppTargetEvents.add(appTargetEvent); } bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents); return bundle; } /** * Builds the AppTargetEvent for added widgets in a form that can be passed to the widget * predictor. * Also see {@link PredictionHelper} */ private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info, ComponentName componentName) { AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName()); AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(), /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build(); return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) .setLaunchLocation(uiSurface).build(); } /** * Returns a filter to match {@link WidgetItem}s that don't exist on the UI surface. */ @NonNull @VisibleForTesting static Predicate<WidgetItem> notOnUiSurfaceFilter( List<AppWidgetProviderInfo> existingWidgets) { Set<ComponentKey> existingComponentKeys = existingWidgets.stream().map( widget -> new ComponentKey(widget.provider, widget.getProfile())).collect( Collectors.toSet()); return widgetItem -> !existingComponentKeys.contains(widgetItem); } /** Provides the predictions returned by the predictor to the registered callback. */ @WorkerThread private void bindPredictions(List<AppTarget> targets, Predicate<WidgetItem> filter, Consumer<List<ItemInfo>> callback) { List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, filter); List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); } /** * Applies the provided filter (e.g. widgets not on workspace) on the predictions returned by * the predictor. */ @VisibleForTesting static List<WidgetItem> filterPredictions(List<AppTarget> predictions, Map<PackageUserKey, List<WidgetItem>> allWidgets, Predicate<WidgetItem> filter) { List<WidgetItem> servicePredictedItems = new ArrayList<>(); List<WidgetItem> localFilteredWidgets = new ArrayList<>(); for (AppTarget prediction : predictions) { List<WidgetItem> widgetsInPackage = allWidgets.get( new PackageUserKey(prediction.getPackageName(), prediction.getUser())); if (widgetsInPackage == null || widgetsInPackage.isEmpty()) { continue; } String className = prediction.getClassName(); if (!TextUtils.isEmpty(className)) { WidgetItem item = widgetsInPackage.stream() .filter(w -> className.equals(w.componentName.getClassName())) .filter(filter) .findFirst().orElse(null); if (item != null) { servicePredictedItems.add(item); continue; } } // No widget was added by the service, try local filtering widgetsInPackage.stream().filter(filter).findFirst() .ifPresent(localFilteredWidgets::add); } if (servicePredictedItems.isEmpty()) { servicePredictedItems.addAll(localFilteredWidgets); } return servicePredictedItems; } /** * Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s. */ private List<ItemInfo> mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems) { List<ItemInfo> items; if (enableCategorizedWidgetSuggestions()) { WidgetRecommendationCategoryProvider categoryProvider = WidgetRecommendationCategoryProvider.newInstance(mContext); items = widgetItems.stream() .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, categoryProvider.getWidgetRecommendationCategory(mContext, it))) .collect(Collectors.toList()); } else { items = widgetItems.stream().map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION)).collect(Collectors.toList()); } return items; } /** Cleans up any open prediction sessions. */ public void clear() { if (mAppPredictor != null) { mAppPredictor.destroy(); mAppPredictor = null; } } } quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt 0 → 100644 +221 −0 File added.Preview size limit exceeded, changes collapsed. Show changes Loading
quickstep/src/com/android/launcher3/WidgetPickerActivity.java +75 −8 Original line number Diff line number Diff line Loading @@ -35,23 +35,31 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.dragndrop.SimpleDragLayer; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.model.WidgetPredictionsRequester; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.BaseWidgetSheet; import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; import com.android.launcher3.widget.picker.WidgetsFullSheet; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; /** An Activity that can host Launcher's widget picker. */ public class WidgetPickerActivity extends BaseActivity { private static final String TAG = "WidgetPickerActivity"; /** * Name of the extra that indicates that a widget being dragged. * Loading @@ -64,14 +72,33 @@ public class WidgetPickerActivity extends BaseActivity { // the intent, then widgets will not be filtered for size. private static final String EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"; private static final String EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"; /** * Widgets currently added by the user in the UI surface. * <p>This allows widget picker to exclude existing widgets from suggestions.</p> */ private static final String EXTRA_ADDED_APP_WIDGETS = "added_app_widgets"; /** * A unique identifier of the surface hosting the widgets; * <p>"widgets" is reserved for home screen surface.</p> * <p>"widgets_hub" is reserved for glanceable hub surface.</p> */ private static final String EXTRA_UI_SURFACE = "ui_surface"; private static final Pattern UI_SURFACE_PATTERN = Pattern.compile("^(widgets|widgets_hub)$"); private SimpleDragLayer<WidgetPickerActivity> mDragLayer; private WidgetsModel mModel; private LauncherAppState mApp; private WidgetPredictionsRequester mWidgetPredictionsRequester; private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {}); private int mDesiredWidgetWidth; private int mDesiredWidgetHeight; private int mWidgetCategoryFilter; @Nullable private String mUiSurface; // Widgets existing on the host surface. @NonNull private List<AppWidgetProviderInfo> mAddedWidgets = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { Loading @@ -80,9 +107,8 @@ public class WidgetPickerActivity extends BaseActivity { getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER); LauncherAppState app = LauncherAppState.getInstance(this); InvariantDeviceProfile idp = app.getInvariantDeviceProfile(); mApp = LauncherAppState.getInstance(this); InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); mDeviceProfile = idp.getDeviceProfile(this); mModel = new WidgetsModel(); Loading @@ -97,6 +123,11 @@ public class WidgetPickerActivity extends BaseActivity { widgetSheet.disableNavBarScrim(true); widgetSheet.addOnCloseListener(this::finish); parseIntentExtras(); refreshAndBindWidgets(); } private void parseIntentExtras() { // A value of 0 for either size means that no filtering will occur in that dimension. If // both values are 0, then no size filtering will occur. mDesiredWidgetWidth = Loading @@ -108,7 +139,15 @@ public class WidgetPickerActivity extends BaseActivity { mWidgetCategoryFilter = getIntent().getIntExtra(AppWidgetManager.EXTRA_CATEGORY_FILTER, 0); refreshAndBindWidgets(); String uiSurfaceParam = getIntent().getStringExtra(EXTRA_UI_SURFACE); if (uiSurfaceParam != null && UI_SURFACE_PATTERN.matcher(uiSurfaceParam).matches()) { mUiSurface = uiSurfaceParam; } ArrayList<AppWidgetProviderInfo> addedWidgets = getIntent().getParcelableArrayListExtra( EXTRA_ADDED_APP_WIDGETS, AppWidgetProviderInfo.class); if (addedWidgets != null) { mAddedWidgets = addedWidgets; } } @NonNull Loading Loading @@ -179,11 +218,12 @@ public class WidgetPickerActivity extends BaseActivity { }; } /** Updates the model with widgets and provides them after applying the provided filter. */ private void refreshAndBindWidgets() { MODEL_EXECUTOR.execute(() -> { LauncherAppState app = LauncherAppState.getInstance(this); mModel.update(app, null); final ArrayList<WidgetsListBaseEntry> widgets = final List<WidgetsListBaseEntry> allWidgets = mModel.getFilteredWidgetsListForPicker( app.getContext(), /*widgetItemFilter=*/ widget -> { Loading @@ -193,10 +233,37 @@ public class WidgetPickerActivity extends BaseActivity { return verdict.isAcceptable; } ); MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); bindWidgets(allWidgets); if (mUiSurface != null) { Map<PackageUserKey, List<WidgetItem>> allWidgetsMap = allWidgets.stream() .filter(WidgetsListHeaderEntry.class::isInstance) .collect(Collectors.toMap( entry -> PackageUserKey.fromPackageItemInfo(entry.mPkgItem), entry -> entry.mWidgets) ); mWidgetPredictionsRequester = new WidgetPredictionsRequester(app.getContext(), mUiSurface, allWidgetsMap); mWidgetPredictionsRequester.request(mAddedWidgets, this::bindRecommendedWidgets); } }); } private void bindWidgets(List<WidgetsListBaseEntry> widgets) { MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets)); } private void bindRecommendedWidgets(List<ItemInfo> recommendedWidgets) { MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setRecommendedWidgets(recommendedWidgets)); } @Override protected void onDestroy() { super.onDestroy(); if (mWidgetPredictionsRequester != null) { mWidgetPredictionsRequester.clear(); } } private WidgetAcceptabilityVerdict isWidgetAcceptable(WidgetItem widget) { final AppWidgetProviderInfo info = widget.widgetInfo; if (info == null) { Loading
quickstep/src/com/android/launcher3/model/WidgetPredictionsRequester.java 0 → 100644 +233 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.Flags.enableCategorizedWidgetSuggestions; 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 android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionManager; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; import android.os.Bundle; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; 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.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; /** * Works with app predictor to fetch and process widget predictions displayed in a standalone * widget picker activity for a UI surface. */ public class WidgetPredictionsRequester { private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; @Nullable private AppPredictor mAppPredictor; private final Context mContext; @NonNull private final String mUiSurface; @NonNull private final Map<PackageUserKey, List<WidgetItem>> mAllWidgets; public WidgetPredictionsRequester(Context context, @NonNull String uiSurface, @NonNull Map<PackageUserKey, List<WidgetItem>> allWidgets) { mContext = context; mUiSurface = uiSurface; mAllWidgets = Collections.unmodifiableMap(allWidgets); } /** * Requests predictions from the app predictions manager and registers the provided callback to * receive updates when predictions are available. * * @param existingWidgets widgets that are currently added to the surface; * @param callback consumer of prediction results to be called when predictions are * available */ public void request(List<AppWidgetProviderInfo> existingWidgets, Consumer<List<ItemInfo>> callback) { Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface); Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets); MODEL_EXECUTOR.execute(() -> { clear(); AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class); if (apm == null) { return; } mAppPredictor = apm.createAppPredictionSession( new AppPredictionContext.Builder(mContext) .setUiSurface(mUiSurface) .setExtras(bundle) .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) .build()); mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, targets -> bindPredictions(targets, filter, callback)); mAppPredictor.requestPredictionUpdate(); }); } /** * Returns a bundle that can be passed in a prediction session * * @param addedWidgets widgets that are already added by the user in the ui surface * @param uiSurface a unique identifier of the surface hosting widgets; format * "widgets_xx"; note - "widgets" is reserved for home screen surface. */ @VisibleForTesting static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets, String uiSurface) { Bundle bundle = new Bundle(); ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>(); for (AppWidgetProviderInfo info : addedWidgets) { ComponentName componentName = info.provider; AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName); addedAppTargetEvents.add(appTargetEvent); } bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents); return bundle; } /** * Builds the AppTargetEvent for added widgets in a form that can be passed to the widget * predictor. * Also see {@link PredictionHelper} */ private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info, ComponentName componentName) { AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName()); AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(), /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build(); return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN) .setLaunchLocation(uiSurface).build(); } /** * Returns a filter to match {@link WidgetItem}s that don't exist on the UI surface. */ @NonNull @VisibleForTesting static Predicate<WidgetItem> notOnUiSurfaceFilter( List<AppWidgetProviderInfo> existingWidgets) { Set<ComponentKey> existingComponentKeys = existingWidgets.stream().map( widget -> new ComponentKey(widget.provider, widget.getProfile())).collect( Collectors.toSet()); return widgetItem -> !existingComponentKeys.contains(widgetItem); } /** Provides the predictions returned by the predictor to the registered callback. */ @WorkerThread private void bindPredictions(List<AppTarget> targets, Predicate<WidgetItem> filter, Consumer<List<ItemInfo>> callback) { List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, filter); List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions); MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions)); } /** * Applies the provided filter (e.g. widgets not on workspace) on the predictions returned by * the predictor. */ @VisibleForTesting static List<WidgetItem> filterPredictions(List<AppTarget> predictions, Map<PackageUserKey, List<WidgetItem>> allWidgets, Predicate<WidgetItem> filter) { List<WidgetItem> servicePredictedItems = new ArrayList<>(); List<WidgetItem> localFilteredWidgets = new ArrayList<>(); for (AppTarget prediction : predictions) { List<WidgetItem> widgetsInPackage = allWidgets.get( new PackageUserKey(prediction.getPackageName(), prediction.getUser())); if (widgetsInPackage == null || widgetsInPackage.isEmpty()) { continue; } String className = prediction.getClassName(); if (!TextUtils.isEmpty(className)) { WidgetItem item = widgetsInPackage.stream() .filter(w -> className.equals(w.componentName.getClassName())) .filter(filter) .findFirst().orElse(null); if (item != null) { servicePredictedItems.add(item); continue; } } // No widget was added by the service, try local filtering widgetsInPackage.stream().filter(filter).findFirst() .ifPresent(localFilteredWidgets::add); } if (servicePredictedItems.isEmpty()) { servicePredictedItems.addAll(localFilteredWidgets); } return servicePredictedItems; } /** * Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s. */ private List<ItemInfo> mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems) { List<ItemInfo> items; if (enableCategorizedWidgetSuggestions()) { WidgetRecommendationCategoryProvider categoryProvider = WidgetRecommendationCategoryProvider.newInstance(mContext); items = widgetItems.stream() .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION, categoryProvider.getWidgetRecommendationCategory(mContext, it))) .collect(Collectors.toList()); } else { items = widgetItems.stream().map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION)).collect(Collectors.toList()); } return items; } /** Cleans up any open prediction sessions. */ public void clear() { if (mAppPredictor != null) { mAppPredictor.destroy(); mAppPredictor = null; } } }
quickstep/tests/src/com/android/launcher3/model/WidgetsPredictionsRequesterTest.kt 0 → 100644 +221 −0 File added.Preview size limit exceeded, changes collapsed. Show changes