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