1 /* 2 * Copyright (C) 2020 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 package com.android.launcher3.model; 17 18 import static android.text.format.DateUtils.DAY_IN_MILLIS; 19 import static android.text.format.DateUtils.formatElapsedTime; 20 21 import static com.android.launcher3.LauncherPrefs.getDevicePrefs; 22 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 23 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_PREDICTION; 24 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_PREDICTION; 25 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 26 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 27 import static com.android.launcher3.hybridhotseat.HotseatPredictionModel.convertDataModelToAppTargetBundle; 28 import static com.android.launcher3.model.PredictionHelper.getAppTargetFromItemInfo; 29 import static com.android.launcher3.model.PredictionHelper.wrapAppTargetWithItemLocation; 30 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 31 32 import static java.util.stream.Collectors.toCollection; 33 34 import android.app.StatsManager; 35 import android.app.prediction.AppPredictionContext; 36 import android.app.prediction.AppPredictionManager; 37 import android.app.prediction.AppPredictor; 38 import android.app.prediction.AppTarget; 39 import android.app.prediction.AppTargetEvent; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.SharedPreferences; 43 import android.content.pm.LauncherActivityInfo; 44 import android.content.pm.LauncherApps; 45 import android.content.pm.ShortcutInfo; 46 import android.os.Bundle; 47 import android.os.UserHandle; 48 import android.util.Log; 49 import android.util.StatsEvent; 50 51 import androidx.annotation.Nullable; 52 import androidx.annotation.WorkerThread; 53 54 import com.android.launcher3.InvariantDeviceProfile; 55 import com.android.launcher3.LauncherAppState; 56 import com.android.launcher3.logger.LauncherAtom; 57 import com.android.launcher3.logging.InstanceId; 58 import com.android.launcher3.logging.InstanceIdSequence; 59 import com.android.launcher3.model.BgDataModel.FixedContainerItems; 60 import com.android.launcher3.model.data.AppInfo; 61 import com.android.launcher3.model.data.FolderInfo; 62 import com.android.launcher3.model.data.ItemInfo; 63 import com.android.launcher3.model.data.WorkspaceItemInfo; 64 import com.android.launcher3.shortcuts.ShortcutKey; 65 import com.android.launcher3.util.IntSparseArrayMap; 66 import com.android.launcher3.util.PersistedItemArray; 67 import com.android.quickstep.logging.SettingsChangeLogger; 68 import com.android.quickstep.logging.StatsLogCompatManager; 69 import com.android.systemui.shared.system.SysUiStatsLog; 70 71 import java.util.ArrayList; 72 import java.util.Collections; 73 import java.util.List; 74 import java.util.Map; 75 import java.util.Objects; 76 import java.util.stream.IntStream; 77 78 /** 79 * Model delegate which loads prediction items 80 */ 81 public class QuickstepModelDelegate extends ModelDelegate { 82 83 public static final String LAST_PREDICTION_ENABLED_STATE = "last_prediction_enabled_state"; 84 private static final String LAST_SNAPSHOT_TIME_MILLIS = "LAST_SNAPSHOT_TIME_MILLIS"; 85 private static final String BUNDLE_KEY_ADDED_APP_WIDGETS = "added_app_widgets"; 86 private static final int NUM_OF_RECOMMENDED_WIDGETS_PREDICATION = 20; 87 88 private static final boolean IS_DEBUG = false; 89 private static final String TAG = "QuickstepModelDelegate"; 90 91 private final PredictorState mAllAppsState = 92 new PredictorState(CONTAINER_PREDICTION, "all_apps_predictions"); 93 private final PredictorState mHotseatState = 94 new PredictorState(CONTAINER_HOTSEAT_PREDICTION, "hotseat_predictions"); 95 private final PredictorState mWidgetsRecommendationState = 96 new PredictorState(CONTAINER_WIDGETS_PREDICTION, "widgets_prediction"); 97 98 private final InvariantDeviceProfile mIDP; 99 private final AppEventProducer mAppEventProducer; 100 private final StatsManager mStatsManager; 101 private final Context mContext; 102 103 protected boolean mActive = false; 104 QuickstepModelDelegate(Context context)105 public QuickstepModelDelegate(Context context) { 106 mContext = context; 107 mAppEventProducer = new AppEventProducer(context, this::onAppTargetEvent); 108 109 mIDP = InvariantDeviceProfile.INSTANCE.get(context); 110 StatsLogCompatManager.LOGS_CONSUMER.add(mAppEventProducer); 111 mStatsManager = context.getSystemService(StatsManager.class); 112 } 113 114 @Override loadHotseatItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts)115 public void loadHotseatItems(UserManagerState ums, 116 Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) { 117 // TODO: Implement caching and preloading 118 super.loadHotseatItems(ums, pinnedShortcuts); 119 120 WorkspaceItemFactory hotseatFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, 121 mIDP.numDatabaseHotseatIcons, mHotseatState.containerId); 122 FixedContainerItems hotseatItems = new FixedContainerItems(mHotseatState.containerId, 123 mHotseatState.storage.read(mApp.getContext(), hotseatFactory, ums.allUsers::get)); 124 mDataModel.extraItems.put(mHotseatState.containerId, hotseatItems); 125 } 126 127 @Override loadAllAppsItems(UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts)128 public void loadAllAppsItems(UserManagerState ums, 129 Map<ShortcutKey, ShortcutInfo> pinnedShortcuts) { 130 // TODO: Implement caching and preloading 131 super.loadAllAppsItems(ums, pinnedShortcuts); 132 133 WorkspaceItemFactory allAppsFactory = new WorkspaceItemFactory(mApp, ums, pinnedShortcuts, 134 mIDP.numDatabaseAllAppsColumns, mAllAppsState.containerId); 135 FixedContainerItems allAppsPredictionItems = new FixedContainerItems( 136 mAllAppsState.containerId, mAllAppsState.storage.read(mApp.getContext(), 137 allAppsFactory, ums.allUsers::get)); 138 mDataModel.extraItems.put(mAllAppsState.containerId, allAppsPredictionItems); 139 } 140 141 @Override loadWidgetsRecommendationItems()142 public void loadWidgetsRecommendationItems() { 143 // TODO: Implement caching and preloading 144 super.loadWidgetsRecommendationItems(); 145 146 // Widgets prediction isn't used frequently. And thus, it is not persisted on disk. 147 mDataModel.extraItems.put(mWidgetsRecommendationState.containerId, 148 new FixedContainerItems(mWidgetsRecommendationState.containerId, 149 new ArrayList<>())); 150 } 151 152 @Override markActive()153 public void markActive() { 154 super.markActive(); 155 mActive = true; 156 } 157 158 @Override workspaceLoadComplete()159 public void workspaceLoadComplete() { 160 super.workspaceLoadComplete(); 161 recreatePredictors(); 162 } 163 164 @Override 165 @WorkerThread modelLoadComplete()166 public void modelLoadComplete() { 167 super.modelLoadComplete(); 168 169 // Log snapshot of the model 170 SharedPreferences prefs = getDevicePrefs(mApp.getContext()); 171 long lastSnapshotTimeMillis = prefs.getLong(LAST_SNAPSHOT_TIME_MILLIS, 0); 172 // Log snapshot only if previous snapshot was older than a day 173 long now = System.currentTimeMillis(); 174 if (now - lastSnapshotTimeMillis < DAY_IN_MILLIS) { 175 if (IS_DEBUG) { 176 String elapsedTime = formatElapsedTime((now - lastSnapshotTimeMillis) / 1000); 177 Log.d(TAG, String.format( 178 "Skipped snapshot logging since previous snapshot was %s old.", 179 elapsedTime)); 180 } 181 } else { 182 IntSparseArrayMap<ItemInfo> itemsIdMap; 183 synchronized (mDataModel) { 184 itemsIdMap = mDataModel.itemsIdMap.clone(); 185 } 186 InstanceId instanceId = new InstanceIdSequence().newInstanceId(); 187 for (ItemInfo info : itemsIdMap) { 188 FolderInfo parent = getContainer(info, itemsIdMap); 189 StatsLogCompatManager.writeSnapshot(info.buildProto(parent), instanceId); 190 } 191 additionalSnapshotEvents(instanceId); 192 prefs.edit().putLong(LAST_SNAPSHOT_TIME_MILLIS, now).apply(); 193 } 194 195 // Only register for launcher snapshot logging if this is the primary ModelDelegate 196 // instance, as there will be additional instances that may be destroyed at any time. 197 if (mIsPrimaryInstance) { 198 registerSnapshotLoggingCallback(); 199 } 200 } 201 additionalSnapshotEvents(InstanceId snapshotInstanceId)202 protected void additionalSnapshotEvents(InstanceId snapshotInstanceId){} 203 204 /** 205 * Registers a callback to log launcher workspace layout using Statsd pulled atom. 206 */ registerSnapshotLoggingCallback()207 protected void registerSnapshotLoggingCallback() { 208 if (mStatsManager == null) { 209 Log.d(TAG, "Failed to get StatsManager"); 210 } 211 212 try { 213 mStatsManager.setPullAtomCallback( 214 SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT, 215 null /* PullAtomMetadata */, 216 MODEL_EXECUTOR, 217 (i, eventList) -> { 218 InstanceId instanceId = new InstanceIdSequence().newInstanceId(); 219 IntSparseArrayMap<ItemInfo> itemsIdMap; 220 synchronized (mDataModel) { 221 itemsIdMap = mDataModel.itemsIdMap.clone(); 222 } 223 224 for (ItemInfo info : itemsIdMap) { 225 FolderInfo parent = getContainer(info, itemsIdMap); 226 LauncherAtom.ItemInfo itemInfo = info.buildProto(parent); 227 Log.d(TAG, itemInfo.toString()); 228 StatsEvent statsEvent = StatsLogCompatManager.buildStatsEvent(itemInfo, 229 instanceId); 230 eventList.add(statsEvent); 231 } 232 Log.d(TAG, 233 String.format( 234 "Successfully logged %d workspace items with instanceId=%d", 235 itemsIdMap.size(), instanceId.getId())); 236 additionalSnapshotEvents(instanceId); 237 SettingsChangeLogger.INSTANCE.get(mContext).logSnapshot(instanceId); 238 return StatsManager.PULL_SUCCESS; 239 } 240 ); 241 Log.d(TAG, "Successfully registered for launcher snapshot logging!"); 242 } catch (RuntimeException e) { 243 Log.e(TAG, "Failed to register launcher snapshot logging callback with StatsManager", 244 e); 245 } 246 } 247 getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap)248 private static FolderInfo getContainer(ItemInfo info, IntSparseArrayMap<ItemInfo> itemsIdMap) { 249 if (info.container > 0) { 250 ItemInfo containerInfo = itemsIdMap.get(info.container); 251 252 if (!(containerInfo instanceof FolderInfo)) { 253 Log.e(TAG, String.format( 254 "Item info: %s found with invalid container: %s", 255 info, 256 containerInfo)); 257 } 258 // Allow crash to help debug b/173838775 259 return (FolderInfo) containerInfo; 260 } 261 return null; 262 } 263 264 @Override validateData()265 public void validateData() { 266 super.validateData(); 267 if (mAllAppsState.predictor != null) { 268 mAllAppsState.predictor.requestPredictionUpdate(); 269 } 270 if (mWidgetsRecommendationState.predictor != null) { 271 mWidgetsRecommendationState.predictor.requestPredictionUpdate(); 272 } 273 } 274 275 @Override destroy()276 public void destroy() { 277 super.destroy(); 278 mActive = false; 279 StatsLogCompatManager.LOGS_CONSUMER.remove(mAppEventProducer); 280 if (mIsPrimaryInstance) { 281 mStatsManager.clearPullAtomCallback(SysUiStatsLog.LAUNCHER_LAYOUT_SNAPSHOT); 282 } 283 destroyPredictors(); 284 } 285 destroyPredictors()286 private void destroyPredictors() { 287 mAllAppsState.destroyPredictor(); 288 mHotseatState.destroyPredictor(); 289 mWidgetsRecommendationState.destroyPredictor(); 290 } 291 292 @WorkerThread recreatePredictors()293 private void recreatePredictors() { 294 destroyPredictors(); 295 if (!mActive) { 296 return; 297 } 298 Context context = mApp.getContext(); 299 AppPredictionManager apm = context.getSystemService(AppPredictionManager.class); 300 if (apm == null) { 301 return; 302 } 303 304 registerPredictor(mAllAppsState, apm.createAppPredictionSession( 305 new AppPredictionContext.Builder(context) 306 .setUiSurface("home") 307 .setPredictedTargetCount(mIDP.numDatabaseAllAppsColumns) 308 .build())); 309 310 // TODO: get bundle 311 registerPredictor(mHotseatState, apm.createAppPredictionSession( 312 new AppPredictionContext.Builder(context) 313 .setUiSurface("hotseat") 314 .setPredictedTargetCount(mIDP.numDatabaseHotseatIcons) 315 .setExtras(convertDataModelToAppTargetBundle(context, mDataModel)) 316 .build())); 317 318 registerWidgetsPredictor(apm.createAppPredictionSession( 319 new AppPredictionContext.Builder(context) 320 .setUiSurface("widgets") 321 .setExtras(getBundleForWidgetsOnWorkspace(context, mDataModel)) 322 .setPredictedTargetCount(NUM_OF_RECOMMENDED_WIDGETS_PREDICATION) 323 .build())); 324 } 325 registerPredictor(PredictorState state, AppPredictor predictor)326 private void registerPredictor(PredictorState state, AppPredictor predictor) { 327 state.setTargets(Collections.emptyList()); 328 state.predictor = predictor; 329 state.predictor.registerPredictionUpdates( 330 MODEL_EXECUTOR, t -> handleUpdate(state, t)); 331 state.predictor.requestPredictionUpdate(); 332 } 333 handleUpdate(PredictorState state, List<AppTarget> targets)334 private void handleUpdate(PredictorState state, List<AppTarget> targets) { 335 if (state.setTargets(targets)) { 336 // No diff, skip 337 return; 338 } 339 mApp.getModel().enqueueModelUpdateTask(new PredictionUpdateTask(state, targets)); 340 } 341 registerWidgetsPredictor(AppPredictor predictor)342 private void registerWidgetsPredictor(AppPredictor predictor) { 343 mWidgetsRecommendationState.predictor = predictor; 344 mWidgetsRecommendationState.predictor.registerPredictionUpdates( 345 MODEL_EXECUTOR, targets -> { 346 if (mWidgetsRecommendationState.setTargets(targets)) { 347 // No diff, skip 348 return; 349 } 350 mApp.getModel().enqueueModelUpdateTask( 351 new WidgetsPredictionUpdateTask(mWidgetsRecommendationState, targets)); 352 }); 353 mWidgetsRecommendationState.predictor.requestPredictionUpdate(); 354 } 355 onAppTargetEvent(AppTargetEvent event, int client)356 private void onAppTargetEvent(AppTargetEvent event, int client) { 357 PredictorState state; 358 switch(client) { 359 case CONTAINER_PREDICTION: 360 state = mAllAppsState; 361 break; 362 case CONTAINER_WIDGETS_PREDICTION: 363 state = mWidgetsRecommendationState; 364 break; 365 case CONTAINER_HOTSEAT_PREDICTION: 366 default: 367 state = mHotseatState; 368 break; 369 } 370 if (state.predictor != null) { 371 state.predictor.notifyAppTargetEvent(event); 372 Log.d(TAG, "notifyAppTargetEvent action=" + event.getAction() 373 + " launchLocation=" + event.getLaunchLocation()); 374 } 375 } 376 getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel)377 private Bundle getBundleForWidgetsOnWorkspace(Context context, BgDataModel dataModel) { 378 Bundle bundle = new Bundle(); 379 ArrayList<AppTargetEvent> widgetEvents = 380 dataModel.getAllWorkspaceItems().stream() 381 .filter(PredictionHelper::isTrackedForWidgetPrediction) 382 .map(item -> { 383 AppTarget target = getAppTargetFromItemInfo(context, item); 384 if (target == null) return null; 385 return wrapAppTargetWithItemLocation( 386 target, AppTargetEvent.ACTION_PIN, item); 387 }) 388 .filter(Objects::nonNull) 389 .collect(toCollection(ArrayList::new)); 390 bundle.putParcelableArrayList(BUNDLE_KEY_ADDED_APP_WIDGETS, widgetEvents); 391 return bundle; 392 } 393 394 static class PredictorState { 395 396 public final int containerId; 397 public final PersistedItemArray<ItemInfo> storage; 398 public AppPredictor predictor; 399 400 private List<AppTarget> mLastTargets; 401 PredictorState(int containerId, String storageName)402 PredictorState(int containerId, String storageName) { 403 this.containerId = containerId; 404 storage = new PersistedItemArray<>(storageName); 405 mLastTargets = Collections.emptyList(); 406 } 407 destroyPredictor()408 public void destroyPredictor() { 409 if (predictor != null) { 410 predictor.destroy(); 411 predictor = null; 412 } 413 } 414 415 /** 416 * Sets the new targets and returns true if it was the same as before. 417 */ setTargets(List<AppTarget> newTargets)418 boolean setTargets(List<AppTarget> newTargets) { 419 List<AppTarget> oldTargets = mLastTargets; 420 mLastTargets = newTargets; 421 422 int size = oldTargets.size(); 423 return size == newTargets.size() && IntStream.range(0, size) 424 .allMatch(i -> areAppTargetsSame(oldTargets.get(i), newTargets.get(i))); 425 } 426 } 427 428 /** 429 * Compares two targets for the properties which we care about 430 */ areAppTargetsSame(AppTarget t1, AppTarget t2)431 private static boolean areAppTargetsSame(AppTarget t1, AppTarget t2) { 432 if (!Objects.equals(t1.getPackageName(), t2.getPackageName()) 433 || !Objects.equals(t1.getUser(), t2.getUser()) 434 || !Objects.equals(t1.getClassName(), t2.getClassName())) { 435 return false; 436 } 437 438 ShortcutInfo s1 = t1.getShortcutInfo(); 439 ShortcutInfo s2 = t2.getShortcutInfo(); 440 if (s1 != null) { 441 if (s2 == null || !Objects.equals(s1.getId(), s2.getId())) { 442 return false; 443 } 444 } else if (s2 != null) { 445 return false; 446 } 447 return true; 448 } 449 450 private static class WorkspaceItemFactory implements PersistedItemArray.ItemFactory<ItemInfo> { 451 452 private final LauncherAppState mAppState; 453 private final UserManagerState mUMS; 454 private final Map<ShortcutKey, ShortcutInfo> mPinnedShortcuts; 455 private final int mMaxCount; 456 private final int mContainer; 457 458 private int mReadCount = 0; 459 WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount, int container)460 protected WorkspaceItemFactory(LauncherAppState appState, UserManagerState ums, 461 Map<ShortcutKey, ShortcutInfo> pinnedShortcuts, int maxCount, int container) { 462 mAppState = appState; 463 mUMS = ums; 464 mPinnedShortcuts = pinnedShortcuts; 465 mMaxCount = maxCount; 466 mContainer = container; 467 } 468 469 @Nullable 470 @Override createInfo(int itemType, UserHandle user, Intent intent)471 public ItemInfo createInfo(int itemType, UserHandle user, Intent intent) { 472 if (mReadCount >= mMaxCount) { 473 return null; 474 } 475 switch (itemType) { 476 case ITEM_TYPE_APPLICATION: { 477 LauncherActivityInfo lai = mAppState.getContext() 478 .getSystemService(LauncherApps.class) 479 .resolveActivity(intent, user); 480 if (lai == null) { 481 return null; 482 } 483 AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user)); 484 info.container = mContainer; 485 mAppState.getIconCache().getTitleAndIcon(info, lai, false); 486 mReadCount++; 487 return info.makeWorkspaceItem(mAppState.getContext()); 488 } 489 case ITEM_TYPE_DEEP_SHORTCUT: { 490 ShortcutKey key = ShortcutKey.fromIntent(intent, user); 491 if (key == null) { 492 return null; 493 } 494 ShortcutInfo si = mPinnedShortcuts.get(key); 495 if (si == null) { 496 return null; 497 } 498 WorkspaceItemInfo wii = new WorkspaceItemInfo(si, mAppState.getContext()); 499 wii.container = mContainer; 500 mAppState.getIconCache().getShortcutIcon(wii, si); 501 mReadCount++; 502 return wii; 503 } 504 } 505 return null; 506 } 507 } 508 } 509