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