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