• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION;
20 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
21 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
22 
23 import android.app.prediction.AppPredictionContext;
24 import android.app.prediction.AppPredictionManager;
25 import android.app.prediction.AppPredictor;
26 import android.app.prediction.AppTarget;
27 import android.app.prediction.AppTargetEvent;
28 import android.app.prediction.AppTargetId;
29 import android.appwidget.AppWidgetProviderInfo;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.os.Bundle;
33 import android.text.TextUtils;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.annotation.WorkerThread;
39 
40 import com.android.launcher3.model.data.ItemInfo;
41 import com.android.launcher3.util.ComponentKey;
42 import com.android.launcher3.widget.PendingAddWidgetInfo;
43 import com.android.launcher3.widget.picker.WidgetRecommendationCategoryProvider;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.function.Predicate;
51 import java.util.stream.Collectors;
52 
53 /**
54  * Works with app predictor to fetch and process widget predictions displayed in a standalone
55  * widget picker activity for a UI surface.
56  */
57 public class WidgetPredictionsRequester implements AppPredictor.Callback {
58     private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20;
59     private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets";
60     // container/screenid/[positionx,positiony]/[spanx,spany]
61     // Matches the format passed used by PredictionHelper; But, position and size values aren't
62     // used, so, we pass default values.
63     @VisibleForTesting
64     static final String LAUNCH_LOCATION = "workspace/1/[0,0]/[2,2]";
65 
66     @Nullable
67     private AppPredictor mAppPredictor;
68     private final Context mContext;
69     @NonNull
70     private final String mUiSurface;
71     private boolean mPredictionsAvailable;
72     @Nullable
73     private WidgetPredictionsListener mPredictionsListener = null;
74     @Nullable Predicate<WidgetItem> mFilter = null;
75     @NonNull
76     private final Map<ComponentKey, WidgetItem> mAllWidgets;
77 
WidgetPredictionsRequester(Context context, @NonNull String uiSurface, @NonNull Map<ComponentKey, WidgetItem> allWidgets)78     public WidgetPredictionsRequester(Context context, @NonNull String uiSurface,
79             @NonNull Map<ComponentKey, WidgetItem> allWidgets) {
80         mContext = context;
81         mUiSurface = uiSurface;
82         mAllWidgets = Collections.unmodifiableMap(allWidgets);
83     }
84 
85     // AppPredictor.Callback -> onTargetsAvailable
86     @Override
87     @WorkerThread
onTargetsAvailable(List<AppTarget> targets)88     public void onTargetsAvailable(List<AppTarget> targets) {
89         List<WidgetItem> filteredPredictions = filterPredictions(targets, mAllWidgets, mFilter);
90         List<ItemInfo> mappedPredictions = mapWidgetItemsToItemInfo(filteredPredictions);
91 
92         if (!mPredictionsAvailable && mPredictionsListener != null) {
93             mPredictionsAvailable = true;
94             MAIN_EXECUTOR.execute(
95                     () -> mPredictionsListener.onPredictionsAvailable(mappedPredictions));
96         }
97     }
98 
99     /**
100      * Requests one time predictions from the app predictions manager and invokes provided callback
101      * once predictions are available. Any previous requests may be cancelled.
102      *
103      * @param existingWidgets widgets that are currently added to the surface;
104      * @param listener        consumer of prediction results to be called when predictions are
105      *                        available; any previous listener will no longer receive updates.
106      */
107     @WorkerThread // e.g. MODEL_EXECUTOR
request(List<AppWidgetProviderInfo> existingWidgets, WidgetPredictionsListener listener)108     public void request(List<AppWidgetProviderInfo> existingWidgets,
109             WidgetPredictionsListener listener) {
110         clear();
111         mPredictionsListener = listener;
112         mFilter = notOnUiSurfaceFilter(existingWidgets);
113 
114         AppPredictionManager apm = mContext.getSystemService(AppPredictionManager.class);
115         if (apm == null) {
116             return;
117         }
118 
119         Bundle bundle = buildBundleForPredictionSession(existingWidgets);
120         mAppPredictor = apm.createAppPredictionSession(
121                 new AppPredictionContext.Builder(mContext)
122                         .setUiSurface(mUiSurface)
123                         .setExtras(bundle)
124                         .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION)
125                         .build());
126         mAppPredictor.registerPredictionUpdates(MODEL_EXECUTOR, /*callback=*/ this);
127         mAppPredictor.requestPredictionUpdate();
128     }
129 
130     /**
131      * Returns a bundle that can be passed in a prediction session
132      *
133      * @param addedWidgets widgets that are already added by the user in the ui surface
134      */
135     @VisibleForTesting
buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets)136     static Bundle buildBundleForPredictionSession(List<AppWidgetProviderInfo> addedWidgets) {
137         Bundle bundle = new Bundle();
138         ArrayList<AppTargetEvent> addedAppTargetEvents = new ArrayList<>();
139         for (AppWidgetProviderInfo info : addedWidgets) {
140             ComponentName componentName = info.provider;
141             AppTargetEvent appTargetEvent = buildAppTargetEvent(info, componentName);
142             addedAppTargetEvents.add(appTargetEvent);
143         }
144         bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, addedAppTargetEvents);
145         return bundle;
146     }
147 
148     /**
149      * Builds the AppTargetEvent for added widgets in a form that can be passed to the widget
150      * predictor.
151      * Also see {@link PredictionHelper}
152      */
buildAppTargetEvent(AppWidgetProviderInfo info, ComponentName componentName)153     private static AppTargetEvent buildAppTargetEvent(AppWidgetProviderInfo info,
154             ComponentName componentName) {
155         AppTargetId appTargetId = new AppTargetId("widget:" + componentName.getPackageName());
156         AppTarget appTarget = new AppTarget.Builder(appTargetId, componentName.getPackageName(),
157                 /*user=*/ info.getProfile()).setClassName(componentName.getClassName()).build();
158         return new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_PIN).setLaunchLocation(
159                 LAUNCH_LOCATION).build();
160     }
161 
162     /**
163      * Returns a filter to match {@link WidgetItem}s that don't exist on the UI surface.
164      */
165     @NonNull
166     @VisibleForTesting
notOnUiSurfaceFilter( List<AppWidgetProviderInfo> existingWidgets)167     static Predicate<WidgetItem> notOnUiSurfaceFilter(
168             List<AppWidgetProviderInfo> existingWidgets) {
169         Set<ComponentKey> existingComponentKeys = existingWidgets.stream().map(
170                 widget -> new ComponentKey(widget.provider, widget.getProfile())).collect(
171                 Collectors.toSet());
172         return widgetItem -> !existingComponentKeys.contains(widgetItem);
173     }
174 
175     /**
176      * Applies the provided filter (e.g. widgets not on workspace) on the predictions returned by
177      * the predictor.
178      */
179     @VisibleForTesting
filterPredictions(List<AppTarget> predictions, @NonNull Map<ComponentKey, WidgetItem> allWidgets, @Nullable Predicate<WidgetItem> filter)180     static List<WidgetItem> filterPredictions(List<AppTarget> predictions,
181             @NonNull Map<ComponentKey, WidgetItem> allWidgets,
182             @Nullable Predicate<WidgetItem> filter) {
183         List<WidgetItem> servicePredictedItems = new ArrayList<>();
184 
185         for (AppTarget prediction : predictions) {
186             String className = prediction.getClassName();
187             if (!TextUtils.isEmpty(className)) {
188                 WidgetItem widgetItem = allWidgets.get(
189                         new ComponentKey(new ComponentName(prediction.getPackageName(), className),
190                                 prediction.getUser()));
191                 if (widgetItem != null && (filter == null || filter.test(widgetItem))) {
192                     servicePredictedItems.add(widgetItem);
193                 }
194             }
195         }
196 
197         return servicePredictedItems;
198     }
199 
200     /**
201      * Converts the list of {@link WidgetItem}s to the list of {@link ItemInfo}s.
202      */
mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems)203     private List<ItemInfo> mapWidgetItemsToItemInfo(List<WidgetItem> widgetItems) {
204         WidgetRecommendationCategoryProvider categoryProvider =
205                 new WidgetRecommendationCategoryProvider();
206         return widgetItems.stream()
207                 .map(it -> new PendingAddWidgetInfo(it.widgetInfo, CONTAINER_WIDGETS_PREDICTION,
208                         categoryProvider.getWidgetRecommendationCategory(mContext, it)))
209                 .collect(Collectors.toList());
210     }
211 
212     /** Cleans up any open prediction sessions. */
clear()213     public void clear() {
214         if (mAppPredictor != null) {
215             mAppPredictor.unregisterPredictionUpdates(this);
216             mAppPredictor.destroy();
217             mAppPredictor = null;
218         }
219         mPredictionsListener = null;
220         mPredictionsAvailable = false;
221         mFilter = null;
222     }
223 
224     /**
225      * Listener class to listen to updates from the {@link WidgetPredictionsRequester}
226      */
227     public interface WidgetPredictionsListener {
228         /**
229          * Callback method that is called when the predicted widgets are available.
230          * @param predictions list of predicted widgets {@link PendingAddWidgetInfo}
231          */
onPredictionsAvailable(List<ItemInfo> predictions)232         void onPredictionsAvailable(List<ItemInfo> predictions);
233     }
234 }
235