1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.model;
18 
19 import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
20 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
21 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
22 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
23 
24 import android.app.prediction.AppPredictionContext;
25 import android.app.prediction.AppPredictionManager;
26 import android.app.prediction.AppPredictor;
27 import android.app.prediction.AppTarget;
28 import android.app.prediction.AppTargetEvent;
29 import android.app.prediction.AppTargetId;
30 import android.appwidget.AppWidgetProviderInfo;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.os.Bundle;
34 import android.text.TextUtils;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.VisibleForTesting;
39 import androidx.annotation.WorkerThread;
40 
41 import com.android.launcher3.model.data.ItemInfo;
42 import com.android.launcher3.util.ComponentKey;
43 import com.android.launcher3.util.PackageUserKey;
44 import com.android.launcher3.widget.PendingAddWidgetInfo;
45 import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.function.Consumer;
53 import java.util.function.Predicate;
54 import java.util.stream.Collectors;
55 
56 /**
57  * Works with app predictor to fetch and process widget predictions displayed in a standalone
58  * widget picker activity for a UI surface.
59  */
60 public class WidgetPredictionsRequester {
61     private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
62     private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets";
63 
64     @Nullable
65     private AppPredictor mAppPredictor;
66     private final Context mContext;
67     @NonNull
68     private final String mUiSurface;
69     @NonNull
70     private final Map<PackageUserKey, List<WidgetItem>> mAllWidgets;
71 
WidgetPredictionsRequester(Context context, @NonNull String uiSurface, @NonNull Map<PackageUserKey, List<WidgetItem>> allWidgets)72     public WidgetPredictionsRequester(Context context, @NonNull String uiSurface,
73             @NonNull Map<PackageUserKey, List<WidgetItem>> allWidgets) {
74         mContext = context;
75         mUiSurface = uiSurface;
76         mAllWidgets = Collections.unmodifiableMap(allWidgets);
77     }
78 
79     /**
80      * Requests predictions from the app predictions manager and registers the provided callback to
81      * receive updates when predictions are available.
82      *
83      * @param existingWidgets widgets that are currently added to the surface;
84      * @param callback        consumer of prediction results to be called when predictions are
85      *                        available
86      */
request(List<AppWidgetProviderInfo> existingWidgets, Consumer<List<ItemInfo>> callback)87     public void request(List<AppWidgetProviderInfo> existingWidgets,
88             Consumer<List<ItemInfo>> callback) {
89         Bundle bundle = buildBundleForPredictionSession(existingWidgets, mUiSurface);
90         Predicate<WidgetItem> filter = notOnUiSurfaceFilter(existingWidgets);
91 
92         MODEL_EXECUTOR.execute(() -> {
93             clear();
94             AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
95             if (apm == null) {
96                 return;
97             }
98 
99             mAppPredictor = apm.createAppPredictionSession(
100                     new AppPredictionContext.Builder(mContext)
101                             .setUiSurface(mUiSurface)
102                             .setExtras(bundle)
103                             .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
104                             .build());
105             mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR,
106                     targets -> bindPredictions(targets, filter, callback));
107             mAppPredictor.requestPredictionUpdate();
108         });
109     }
110 
111     /**
112      * Returns a bundle that can be passed in a prediction session
113      *
114      * @param addedWidgets widgets that are already added by the user in the ui surface
115      * @param uiSurface    a unique identifier of the surface hosting widgets; format
116      *                     "widgets_xx"; note - "widgets" is reserved for home screen surface.
117      */
118     @VisibleForTesting
buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets, String uiSurface)119     static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets,
120             String uiSurface) {
121         Bundle bundle = new Bundle();
122         ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>();
123         for (AppWidgetProviderInfo info : addedWidgets) {
124             ComponentName componentName = info.provider;
125             AppTargetEvent appTargetEvent = buildAppTargetEvent(uiSurface, info, componentName);
126             addedAppTargetEvents.add(appTargetEvent);
127         }
128         bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents);
129         return bundle;
130     }
131 
132     /**
133      * Builds the AppTargetEvent for added widgets in a form that can be passed to the widget
134      * predictor.
135      * Also see {@link PredictionHelper}
136      */
buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info, ComponentName componentName)137     private static AppTargetEvent buildAppTargetEvent(String uiSurface, AppWidgetProviderInfo info,
138             ComponentName componentName) {
139         AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName());
140         AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(),
141                 /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build();
142         return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN)
143                 .setLaunchLocation(uiSurface).build();
144     }
145 
146     /**
147      * Returns a filter to match {@link WidgetItem}s that don't exist on the UI surface.
148      */
149     @NonNull
150     @VisibleForTesting
notOnUiSurfaceFilter( List<AppWidgetProviderInfo> existingWidgets)151     static Predicate<WidgetItem> notOnUiSurfaceFilter(
152             List<AppWidgetProviderInfo> existingWidgets) {
153         Set<ComponentKey> existingComponentKeys = existingWidgets.stream().map(
154                 widget -> new ComponentKey(widget.provider, widget.getProfile())).collect(
155                 Collectors.toSet());
156         return widgetItem -> !existingComponentKeys.contains(widgetItem);
157     }
158 
159     /** Provides the predictions returned by the predictor to the registered callback. */
160     @WorkerThread
bindPredictions(List<AppTarget> targets, Predicate<WidgetItem> filter, Consumer<List<ItemInfo>> callback)161     private void bindPredictions(List<AppTarget> targets, Predicate<WidgetItem> filter,
162             Consumer<List<ItemInfo>> callback) {
163         List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, filter);
164         List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions);
165 
166         MAIN_EXECUTOR.execute(() -> callback.accept(mappedPredictions));
167     }
168 
169     /**
170      * Applies the provided filter (e.g. widgets not on workspace) on the predictions returned by
171      * the predictor.
172      */
173     @VisibleForTesting
filterPredictions(List<AppTarget> predictions, Map<PackageUserKey, List<WidgetItem>> allWidgets, Predicate<WidgetItem> filter)174     static List<WidgetItem> filterPredictions(List<AppTarget> predictions,
175             Map<PackageUserKey, List<WidgetItem>> allWidgets, Predicate<WidgetItem> filter) {
176         List<WidgetItem> servicePredictedItems = new ArrayList<>();
177         List<WidgetItem> localFilteredWidgets = new ArrayList<>();
178 
179         for (AppTarget prediction : predictions) {
180             List<WidgetItem> widgetsInPackage = allWidgets.get(
181                     new PackageUserKey(prediction.getPackageName(), prediction.getUser()));
182             if (widgetsInPackage == null || widgetsInPackage.isEmpty()) {
183                 continue;
184             }
185             String className = prediction.getClassName();
186             if (!TextUtils.isEmpty(className)) {
187                 WidgetItem item = widgetsInPackage.stream()
188                         .filter(w -> className.equals(w.componentName.getClassName()))
189                         .filter(filter)
190                         .findFirst().orElse(null);
191                 if (item != null) {
192                     servicePredictedItems.add(item);
193                     continue;
194                 }
195             }
196             // No widget was added by the service, try local filtering
197             widgetsInPackage.stream().filter(filter).findFirst()
198                     .ifPresent(localFilteredWidgets::add);
199         }
200         if (servicePredictedItems.isEmpty()) {
201             servicePredictedItems.addAll(localFilteredWidgets);
202         }
203 
204         return servicePredictedItems;
205     }
206 
207     /**
208      * Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s.
209      */
mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems)210     private List<ItemInfo> mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems) {
211         List<ItemInfo> items;
212         if (enableCategorizedWidgetSuggestions()) {
213             WidgetRecommendationCategoryProvider categoryProvider =
214                     WidgetRecommendationCategoryProvider.newInstance(mContext);
215             items = widgetItems.stream()
216                     .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
217                             categoryProvider.getWidgetRecommendationCategory(mContext, it)))
218                     .collect(Collectors.toList());
219         } else {
220             items = widgetItems.stream().map(it -> new PendingAddWidgetInfo(it.widgetInfo,
221                     CONTAINER_WIDGETS_PREDICTION)).collect(Collectors.toList());
222         }
223         return items;
224     }
225 
226     /** Cleans up any open prediction sessions. */
clear()227     public void clear() {
228         if (mAppPredictor != null) {
229             mAppPredictor.destroy();
230             mAppPredictor = null;
231         }
232     }
233 }
234