1 /* 2 * Copyright (C) 2019 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.hybridhotseat; 17 18 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_GRID; 19 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 20 import static com.android.launcher3.LauncherState.NORMAL; 21 import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorSet; 26 import android.animation.ObjectAnimator; 27 import android.app.prediction.AppPredictionContext; 28 import android.app.prediction.AppPredictionManager; 29 import android.app.prediction.AppPredictor; 30 import android.app.prediction.AppTarget; 31 import android.app.prediction.AppTargetEvent; 32 import android.content.ComponentName; 33 import android.os.Process; 34 import android.util.Log; 35 import android.view.HapticFeedbackConstants; 36 import android.view.View; 37 import android.view.ViewGroup; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 42 import com.android.launcher3.DragSource; 43 import com.android.launcher3.DropTarget; 44 import com.android.launcher3.Hotseat; 45 import com.android.launcher3.InvariantDeviceProfile; 46 import com.android.launcher3.Launcher; 47 import com.android.launcher3.LauncherAppState; 48 import com.android.launcher3.LauncherSettings; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.allapps.AllAppsStore; 52 import com.android.launcher3.anim.AnimationSuccessListener; 53 import com.android.launcher3.appprediction.ComponentKeyMapper; 54 import com.android.launcher3.appprediction.DynamicItemCache; 55 import com.android.launcher3.dragndrop.DragController; 56 import com.android.launcher3.dragndrop.DragOptions; 57 import com.android.launcher3.icons.IconCache; 58 import com.android.launcher3.logger.LauncherAtom.ContainerInfo; 59 import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer; 60 import com.android.launcher3.logging.InstanceId; 61 import com.android.launcher3.model.data.AppInfo; 62 import com.android.launcher3.model.data.FolderInfo; 63 import com.android.launcher3.model.data.ItemInfo; 64 import com.android.launcher3.model.data.ItemInfoWithIcon; 65 import com.android.launcher3.model.data.WorkspaceItemInfo; 66 import com.android.launcher3.popup.SystemShortcut; 67 import com.android.launcher3.shortcuts.ShortcutKey; 68 import com.android.launcher3.touch.ItemLongClickListener; 69 import com.android.launcher3.uioverrides.PredictedAppIcon; 70 import com.android.launcher3.uioverrides.QuickstepLauncher; 71 import com.android.launcher3.userevent.nano.LauncherLogProto; 72 import com.android.launcher3.util.ComponentKey; 73 import com.android.launcher3.util.IntArray; 74 import com.android.launcher3.util.OnboardingPrefs; 75 import com.android.launcher3.views.ArrowTipView; 76 import com.android.launcher3.views.Snackbar; 77 78 import java.lang.ref.WeakReference; 79 import java.util.ArrayList; 80 import java.util.Collections; 81 import java.util.List; 82 import java.util.OptionalInt; 83 import java.util.stream.IntStream; 84 85 /** 86 * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows 87 * pinning of predicted apps and manages replacement of predicted apps with user drag. 88 */ 89 public class HotseatPredictionController implements DragController.DragListener, 90 View.OnAttachStateChangeListener, SystemShortcut.Factory<QuickstepLauncher>, 91 InvariantDeviceProfile.OnIDPChangeListener, AllAppsStore.OnUpdateListener, 92 IconCache.ItemInfoUpdateReceiver, DragSource { 93 94 private static final String TAG = "PredictiveHotseat"; 95 private static final boolean DEBUG = false; 96 97 private static final String PREDICTION_CLIENT = "hotseat"; 98 private DropTarget.DragObject mDragObject; 99 private int mHotSeatItemsCount; 100 private int mPredictedSpotsCount = 0; 101 102 private Launcher mLauncher; 103 private final Hotseat mHotseat; 104 105 private final HotseatRestoreHelper mRestoreHelper; 106 107 private List<ComponentKeyMapper> mComponentKeyMappers = new ArrayList<>(); 108 109 private DynamicItemCache mDynamicItemCache; 110 111 private final HotseatPredictionModel mPredictionModel; 112 private AppPredictor mAppPredictor; 113 private AllAppsStore mAllAppsStore; 114 private AnimatorSet mIconRemoveAnimators; 115 private boolean mUIUpdatePaused = false; 116 private boolean mIsDestroyed = false; 117 118 119 private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>(); 120 121 private final View.OnLongClickListener mPredictionLongClickListener = v -> { 122 if (!ItemLongClickListener.canStartDrag(mLauncher)) return false; 123 if (mLauncher.getWorkspace().isSwitchingState()) return false; 124 if (!mLauncher.getOnboardingPrefs().getBoolean( 125 OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN)) { 126 Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, 127 R.string.hotseat_prediction_settings, null, 128 () -> mLauncher.startActivity(getSettingsIntent())); 129 mLauncher.getOnboardingPrefs().markChecked(OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN); 130 mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 131 return true; 132 } 133 // Start the drag 134 mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions()); 135 return true; 136 }; 137 HotseatPredictionController(Launcher launcher)138 public HotseatPredictionController(Launcher launcher) { 139 mLauncher = launcher; 140 mHotseat = launcher.getHotseat(); 141 mAllAppsStore = mLauncher.getAppsView().getAppsStore(); 142 LauncherAppState appState = LauncherAppState.getInstance(launcher); 143 mPredictionModel = (HotseatPredictionModel) appState.getPredictionModel(); 144 mAllAppsStore.addUpdateListener(this); 145 mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction); 146 mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons; 147 launcher.getDeviceProfile().inv.addOnChangeListener(this); 148 mHotseat.addOnAttachStateChangeListener(this); 149 mRestoreHelper = new HotseatRestoreHelper(mLauncher); 150 if (mHotseat.isAttachedToWindow()) { 151 onViewAttachedToWindow(mHotseat); 152 } 153 } 154 155 /** 156 * Shows appropriate hotseat education based on prediction enabled and migration states. 157 */ showEdu()158 public void showEdu() { 159 mLauncher.getStateManager().goToState(NORMAL, true, () -> { 160 if (mComponentKeyMappers.isEmpty()) { 161 // launcher has empty predictions set 162 Snackbar.show(mLauncher, R.string.hotsaet_tip_prediction_disabled, 163 R.string.hotseat_prediction_settings, null, 164 () -> mLauncher.startActivity(getSettingsIntent())); 165 } else if (getPredictedIcons().size() >= (mHotSeatItemsCount + 1) / 2) { 166 showDiscoveryTip(); 167 } else { 168 HotseatEduController eduController = new HotseatEduController(mLauncher, 169 mRestoreHelper, 170 this::createPredictor); 171 eduController.setPredictedApps(mapToWorkspaceItemInfo(mComponentKeyMappers)); 172 eduController.showEdu(); 173 } 174 }); 175 } 176 177 /** 178 * Shows educational tip for hotseat if user does not go through Tips app. 179 */ showDiscoveryTip()180 private void showDiscoveryTip() { 181 if (getPredictedIcons().isEmpty()) { 182 new ArrowTipView(mLauncher).show( 183 mLauncher.getString(R.string.hotseat_tip_no_empty_slots), mHotseat.getTop()); 184 } else { 185 Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled, 186 R.string.hotseat_prediction_settings, null, 187 () -> mLauncher.startActivity(getSettingsIntent())); 188 } 189 } 190 191 /** 192 * Returns if hotseat client has predictions 193 */ hasPredictions()194 public boolean hasPredictions() { 195 return !mComponentKeyMappers.isEmpty(); 196 } 197 198 @Override onViewAttachedToWindow(View view)199 public void onViewAttachedToWindow(View view) { 200 mLauncher.getDragController().addDragListener(this); 201 } 202 203 @Override onViewDetachedFromWindow(View view)204 public void onViewDetachedFromWindow(View view) { 205 mLauncher.getDragController().removeDragListener(this); 206 } 207 fillGapsWithPrediction()208 private void fillGapsWithPrediction() { 209 fillGapsWithPrediction(false, null); 210 } 211 fillGapsWithPrediction(boolean animate, Runnable callback)212 private void fillGapsWithPrediction(boolean animate, Runnable callback) { 213 if (mUIUpdatePaused || mDragObject != null) { 214 return; 215 } 216 List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers); 217 if (mComponentKeyMappers.isEmpty() != predictedApps.isEmpty()) { 218 // Safely ignore update as AppsList is not ready yet. This will called again once 219 // apps are ready (HotseatPredictionController#onAppsUpdated) 220 return; 221 } 222 int predictionIndex = 0; 223 ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>(); 224 // make sure predicted icon removal and filling predictions don't step on each other 225 if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) { 226 mIconRemoveAnimators.addListener(new AnimationSuccessListener() { 227 @Override 228 public void onAnimationSuccess(Animator animator) { 229 fillGapsWithPrediction(animate, callback); 230 mIconRemoveAnimators.removeListener(this); 231 } 232 }); 233 return; 234 } 235 for (int rank = 0; rank < mHotSeatItemsCount; rank++) { 236 View child = mHotseat.getChildAt( 237 mHotseat.getCellXFromOrder(rank), 238 mHotseat.getCellYFromOrder(rank)); 239 240 if (child != null && !isPredictedIcon(child)) { 241 continue; 242 } 243 if (predictedApps.size() <= predictionIndex) { 244 // Remove predicted apps from the past 245 if (isPredictedIcon(child)) { 246 mHotseat.removeView(child); 247 } 248 continue; 249 } 250 WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++); 251 if (isPredictedIcon(child) && child.isEnabled()) { 252 PredictedAppIcon icon = (PredictedAppIcon) child; 253 icon.applyFromWorkspaceItem(predictedItem); 254 icon.finishBinding(mPredictionLongClickListener); 255 } else { 256 newItems.add(predictedItem); 257 } 258 preparePredictionInfo(predictedItem, rank); 259 } 260 mPredictedSpotsCount = predictionIndex; 261 bindItems(newItems, animate, callback); 262 } 263 bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback)264 private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) { 265 AnimatorSet animationSet = new AnimatorSet(); 266 for (WorkspaceItemInfo item : itemsToAdd) { 267 PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item); 268 mLauncher.getWorkspace().addInScreenFromBind(icon, item); 269 icon.finishBinding(mPredictionLongClickListener); 270 if (animate) { 271 animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1)); 272 } 273 } 274 if (animate) { 275 if (callback != null) { 276 animationSet.addListener(AnimationSuccessListener.forRunnable(callback)); 277 } 278 animationSet.start(); 279 } else { 280 if (callback != null) callback.run(); 281 } 282 } 283 284 /** 285 * Unregisters callbacks and frees resources 286 */ destroy()287 public void destroy() { 288 mIsDestroyed = true; 289 mAllAppsStore.removeUpdateListener(this); 290 mLauncher.getDeviceProfile().inv.removeOnChangeListener(this); 291 mHotseat.removeOnAttachStateChangeListener(this); 292 if (mAppPredictor != null) { 293 mAppPredictor.destroy(); 294 } 295 } 296 297 /** 298 * start and pauses predicted apps update on the hotseat 299 */ setPauseUIUpdate(boolean paused)300 public void setPauseUIUpdate(boolean paused) { 301 mUIUpdatePaused = paused; 302 if (!paused) { 303 fillGapsWithPrediction(); 304 } 305 } 306 307 /** 308 * Creates App Predictor with all the current apps pinned on the hotseat 309 */ createPredictor()310 public void createPredictor() { 311 AppPredictionManager apm = mLauncher.getSystemService(AppPredictionManager.class); 312 if (apm == null) { 313 return; 314 } 315 if (mAppPredictor != null) { 316 mAppPredictor.destroy(); 317 mAppPredictor = null; 318 } 319 WeakReference<HotseatPredictionController> controllerRef = new WeakReference<>(this); 320 321 322 mPredictionModel.createBundle(bundle -> { 323 if (mIsDestroyed) return; 324 mAppPredictor = apm.createAppPredictionSession( 325 new AppPredictionContext.Builder(mLauncher) 326 .setUiSurface(PREDICTION_CLIENT) 327 .setPredictedTargetCount(mHotSeatItemsCount) 328 .setExtras(bundle) 329 .build()); 330 mAppPredictor.registerPredictionUpdates( 331 mLauncher.getApplicationContext().getMainExecutor(), 332 list -> { 333 if (controllerRef.get() != null) { 334 controllerRef.get().setPredictedApps(list); 335 } 336 }); 337 mAppPredictor.requestPredictionUpdate(); 338 }); 339 setPauseUIUpdate(false); 340 } 341 342 /** 343 * Create WorkspaceItemInfo objects and binds PredictedAppIcon views for cached predicted items. 344 */ showCachedItems(List<AppInfo> apps, IntArray ranks)345 public void showCachedItems(List<AppInfo> apps, IntArray ranks) { 346 if (hasPredictions() && mAppPredictor != null) { 347 mAppPredictor.requestPredictionUpdate(); 348 fillGapsWithPrediction(); 349 return; 350 } 351 int count = Math.min(ranks.size(), apps.size()); 352 List<WorkspaceItemInfo> items = new ArrayList<>(count); 353 for (int i = 0; i < count; i++) { 354 WorkspaceItemInfo item = new WorkspaceItemInfo(apps.get(i)); 355 ComponentKey componentKey = new ComponentKey(item.getTargetComponent(), item.user); 356 preparePredictionInfo(item, ranks.get(i)); 357 items.add(item); 358 359 mComponentKeyMappers.add(new ComponentKeyMapper(componentKey, mDynamicItemCache)); 360 } 361 updateDependencies(); 362 bindItems(items, false, null); 363 } 364 setPredictedApps(List<AppTarget> appTargets)365 private void setPredictedApps(List<AppTarget> appTargets) { 366 mComponentKeyMappers.clear(); 367 if (appTargets.isEmpty()) { 368 mRestoreHelper.restoreBackup(); 369 } 370 StringBuilder predictionLog = new StringBuilder("predictedApps: [\n"); 371 ArrayList<ComponentKey> componentKeys = new ArrayList<>(); 372 for (AppTarget appTarget : appTargets) { 373 ComponentKey key; 374 if (appTarget.getShortcutInfo() != null) { 375 key = ShortcutKey.fromInfo(appTarget.getShortcutInfo()); 376 } else { 377 key = new ComponentKey(new ComponentName(appTarget.getPackageName(), 378 appTarget.getClassName()), appTarget.getUser()); 379 } 380 componentKeys.add(key); 381 predictionLog.append(key.toString()); 382 predictionLog.append(",rank:"); 383 predictionLog.append(appTarget.getRank()); 384 predictionLog.append("\n"); 385 mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache)); 386 } 387 predictionLog.append("]"); 388 if (Utilities.IS_DEBUG_DEVICE) { 389 HotseatFileLog.INSTANCE.get(mLauncher).log(TAG, predictionLog.toString()); 390 } 391 updateDependencies(); 392 fillGapsWithPrediction(); 393 mPredictionModel.cachePredictionComponentKeys(componentKeys); 394 } 395 updateDependencies()396 private void updateDependencies() { 397 mDynamicItemCache.updateDependencies(mComponentKeyMappers, mAllAppsStore, this, 398 mHotSeatItemsCount); 399 } 400 401 /** 402 * Pins a predicted app icon into place. 403 */ pinPrediction(ItemInfo info)404 public void pinPrediction(ItemInfo info) { 405 PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt( 406 mHotseat.getCellXFromOrder(info.rank), 407 mHotseat.getCellYFromOrder(info.rank)); 408 if (icon == null) { 409 return; 410 } 411 WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info); 412 mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo, 413 LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId, 414 workspaceItemInfo.cellX, workspaceItemInfo.cellY); 415 ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start(); 416 icon.pin(workspaceItemInfo); 417 AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(workspaceItemInfo); 418 if (appTarget != null) { 419 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget, 420 AppTargetEvent.ACTION_PIN, workspaceItemInfo)); 421 } 422 } 423 mapToWorkspaceItemInfo( List<ComponentKeyMapper> components)424 private List<WorkspaceItemInfo> mapToWorkspaceItemInfo( 425 List<ComponentKeyMapper> components) { 426 AllAppsStore allAppsStore = mLauncher.getAppsView().getAppsStore(); 427 if (allAppsStore.getApps().length == 0) { 428 return Collections.emptyList(); 429 } 430 431 List<WorkspaceItemInfo> predictedApps = new ArrayList<>(); 432 for (ComponentKeyMapper mapper : components) { 433 ItemInfoWithIcon info = mapper.getApp(allAppsStore); 434 if (info instanceof AppInfo) { 435 WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((AppInfo) info); 436 predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 437 predictedApps.add(predictedApp); 438 } else if (info instanceof WorkspaceItemInfo) { 439 WorkspaceItemInfo predictedApp = new WorkspaceItemInfo((WorkspaceItemInfo) info); 440 predictedApp.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 441 predictedApps.add(predictedApp); 442 } else { 443 if (DEBUG) { 444 Log.e(TAG, "Predicted app not found: " + mapper); 445 } 446 } 447 // Stop at the number of hotseat items 448 if (predictedApps.size() == mHotSeatItemsCount) { 449 break; 450 } 451 } 452 return predictedApps; 453 } 454 getPredictedIcons()455 private List<PredictedAppIcon> getPredictedIcons() { 456 List<PredictedAppIcon> icons = new ArrayList<>(); 457 ViewGroup vg = mHotseat.getShortcutsAndWidgets(); 458 for (int i = 0; i < vg.getChildCount(); i++) { 459 View child = vg.getChildAt(i); 460 if (isPredictedIcon(child)) { 461 icons.add((PredictedAppIcon) child); 462 } 463 } 464 return icons; 465 } 466 removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines, ItemInfo draggedInfo)467 private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines, 468 ItemInfo draggedInfo) { 469 if (mIconRemoveAnimators != null) { 470 mIconRemoveAnimators.end(); 471 } 472 mIconRemoveAnimators = new AnimatorSet(); 473 removeOutlineDrawings(); 474 for (PredictedAppIcon icon : getPredictedIcons()) { 475 if (!icon.isEnabled()) { 476 continue; 477 } 478 if (icon.getTag().equals(draggedInfo)) { 479 mHotseat.removeView(icon); 480 continue; 481 } 482 int rank = ((WorkspaceItemInfo) icon.getTag()).rank; 483 outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing( 484 mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon)); 485 icon.setEnabled(false); 486 ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0); 487 animator.addListener(new AnimationSuccessListener() { 488 @Override 489 public void onAnimationSuccess(Animator animator) { 490 if (icon.getParent() != null) { 491 mHotseat.removeView(icon); 492 } 493 } 494 }); 495 mIconRemoveAnimators.play(animator); 496 } 497 mIconRemoveAnimators.start(); 498 } 499 notifyItemAction(AppTargetEvent event)500 private void notifyItemAction(AppTargetEvent event) { 501 if (mAppPredictor != null) { 502 mAppPredictor.notifyAppTargetEvent(event); 503 } 504 } 505 506 @Override onDragStart(DropTarget.DragObject dragObject, DragOptions options)507 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 508 removePredictedApps(mOutlineDrawings, dragObject.dragInfo); 509 mDragObject = dragObject; 510 if (mOutlineDrawings.isEmpty()) return; 511 for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) { 512 mHotseat.addDelegatedCellDrawing(outlineDrawing); 513 } 514 mHotseat.invalidate(); 515 } 516 517 /** 518 * Unpins pinned app when it's converted into a folder 519 */ folderCreatedFromWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo)520 public void folderCreatedFromWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) { 521 AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo); 522 AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo); 523 if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) { 524 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget, 525 AppTargetEvent.ACTION_PIN, folderInfo)); 526 } 527 // using folder info with isTrackedForPrediction as itemInfo.container is already changed 528 // to folder by this point 529 if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) { 530 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget, 531 AppTargetEvent.ACTION_UNPIN, folderInfo 532 )); 533 } 534 } 535 536 /** 537 * Pins workspace item created when all folder items are removed but one 538 */ folderConvertedToWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo)539 public void folderConvertedToWorkspaceItem(ItemInfo itemInfo, FolderInfo folderInfo) { 540 AppTarget folderTarget = mPredictionModel.getAppTargetFromInfo(folderInfo); 541 AppTarget itemTarget = mPredictionModel.getAppTargetFromInfo(itemInfo); 542 if (folderTarget != null && HotseatPredictionModel.isTrackedForPrediction(folderInfo)) { 543 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(folderTarget, 544 AppTargetEvent.ACTION_UNPIN, folderInfo)); 545 } 546 if (itemTarget != null && HotseatPredictionModel.isTrackedForPrediction(itemInfo)) { 547 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(itemTarget, 548 AppTargetEvent.ACTION_PIN, itemInfo)); 549 } 550 } 551 552 @Override onDragEnd()553 public void onDragEnd() { 554 if (mDragObject == null) { 555 return; 556 } 557 558 ItemInfo dragInfo = mDragObject.dragInfo; 559 if (mDragObject.isMoved()) { 560 AppTarget appTarget = mPredictionModel.getAppTargetFromInfo(dragInfo); 561 //always send pin event first to prevent AiAi from predicting an item moved within 562 // the same page 563 if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction(dragInfo)) { 564 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget, 565 AppTargetEvent.ACTION_PIN, dragInfo)); 566 } 567 if (appTarget != null && HotseatPredictionModel.isTrackedForPrediction( 568 mDragObject.originalDragInfo)) { 569 notifyItemAction(mPredictionModel.wrapAppTargetWithLocation(appTarget, 570 AppTargetEvent.ACTION_UNPIN, mDragObject.originalDragInfo)); 571 } 572 } 573 mDragObject = null; 574 fillGapsWithPrediction(true, this::removeOutlineDrawings); 575 } 576 577 578 @Nullable 579 @Override getShortcut(QuickstepLauncher activity, ItemInfo itemInfo)580 public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity, 581 ItemInfo itemInfo) { 582 if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { 583 return null; 584 } 585 return new PinPrediction(activity, itemInfo); 586 } 587 preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank)588 private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) { 589 itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 590 itemInfo.rank = rank; 591 itemInfo.cellX = mHotseat.getCellXFromOrder(rank); 592 itemInfo.cellY = mHotseat.getCellYFromOrder(rank); 593 itemInfo.screenId = rank; 594 } 595 removeOutlineDrawings()596 private void removeOutlineDrawings() { 597 if (mOutlineDrawings.isEmpty()) return; 598 for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) { 599 mHotseat.removeDelegatedCellDrawing(outlineDrawing); 600 } 601 mHotseat.invalidate(); 602 mOutlineDrawings.clear(); 603 } 604 605 @Override onIdpChanged(int changeFlags, InvariantDeviceProfile profile)606 public void onIdpChanged(int changeFlags, InvariantDeviceProfile profile) { 607 if ((changeFlags & CHANGE_FLAG_GRID) != 0) { 608 this.mHotSeatItemsCount = profile.numHotseatIcons; 609 createPredictor(); 610 } 611 } 612 613 @Override onAppsUpdated()614 public void onAppsUpdated() { 615 fillGapsWithPrediction(); 616 } 617 618 @Override reapplyItemInfo(ItemInfoWithIcon info)619 public void reapplyItemInfo(ItemInfoWithIcon info) { 620 } 621 622 @Override onDropCompleted(View target, DropTarget.DragObject d, boolean success)623 public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) { 624 //Does nothing 625 } 626 627 @Override fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child, ArrayList<LauncherLogProto.Target> parents)628 public void fillInLogContainerData(ItemInfo childInfo, LauncherLogProto.Target child, 629 ArrayList<LauncherLogProto.Target> parents) { 630 mHotseat.fillInLogContainerData(childInfo, child, parents); 631 } 632 633 /** 634 * Logs rank info based on current list of predicted items 635 */ logLaunchedAppRankingInfo(@onNull ItemInfo itemInfo, InstanceId instanceId)636 public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) { 637 if (Utilities.IS_DEBUG_DEVICE) { 638 final String pkg = itemInfo.getTargetComponent() != null 639 ? itemInfo.getTargetComponent().getPackageName() : "unknown"; 640 HotseatFileLog.INSTANCE.get(mLauncher).log("UserEvent", 641 "appLaunch: packageName:" + pkg + ",isWorkApp:" + (itemInfo.user != null 642 && !Process.myUserHandle().equals(itemInfo.user)) 643 + ",launchLocation:" + itemInfo.container); 644 } 645 646 if (itemInfo.getTargetComponent() == null || itemInfo.user == null) { 647 return; 648 } 649 650 final ComponentKey key = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user); 651 652 final List<ComponentKeyMapper> predictedApps = new ArrayList<>(mComponentKeyMappers); 653 OptionalInt rank = IntStream.range(0, predictedApps.size()) 654 .filter(index -> key.equals(predictedApps.get(index).getComponentKey())) 655 .findFirst(); 656 if (!rank.isPresent()) { 657 return; 658 } 659 660 int cardinality = 0; 661 for (PredictedAppIcon icon : getPredictedIcons()) { 662 ItemInfo info = (ItemInfo) icon.getTag(); 663 cardinality |= 1 << info.screenId; 664 } 665 666 PredictedHotseatContainer.Builder containerBuilder = PredictedHotseatContainer.newBuilder(); 667 containerBuilder.setCardinality(cardinality); 668 if (itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) { 669 containerBuilder.setIndex(rank.getAsInt()); 670 } 671 mLauncher.getStatsLogManager().logger() 672 .withInstanceId(instanceId) 673 .withRank(rank.getAsInt()) 674 .withContainerInfo(ContainerInfo.newBuilder() 675 .setPredictedHotseatContainer(containerBuilder) 676 .build()) 677 .log(LAUNCHER_HOTSEAT_RANKED); 678 } 679 680 private class PinPrediction extends SystemShortcut<QuickstepLauncher> { 681 PinPrediction(QuickstepLauncher target, ItemInfo itemInfo)682 private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo) { 683 super(R.drawable.ic_pin, R.string.pin_prediction, target, 684 itemInfo); 685 } 686 687 @Override onClick(View view)688 public void onClick(View view) { 689 dismissTaskMenuView(mTarget); 690 pinPrediction(mItemInfo); 691 } 692 } 693 694 /** 695 * Fill in predicted_rank field based on app prediction. 696 * Only applicable when {@link ItemInfo#itemType} is PREDICTED_HOTSEAT 697 */ encodeHotseatLayoutIntoPredictionRank( @onNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target)698 public static void encodeHotseatLayoutIntoPredictionRank( 699 @NonNull ItemInfo itemInfo, @NonNull LauncherLogProto.Target target) { 700 QuickstepLauncher launcher = QuickstepLauncher.ACTIVITY_TRACKER.getCreatedActivity(); 701 if (launcher == null || launcher.getHotseatPredictionController() == null 702 || itemInfo.getTargetComponent() == null) { 703 return; 704 } 705 HotseatPredictionController controller = launcher.getHotseatPredictionController(); 706 707 final ComponentKey k = new ComponentKey(itemInfo.getTargetComponent(), itemInfo.user); 708 709 final List<ComponentKeyMapper> predictedApps = controller.mComponentKeyMappers; 710 OptionalInt rank = IntStream.range(0, predictedApps.size()) 711 .filter((i) -> k.equals(predictedApps.get(i).getComponentKey())) 712 .findFirst(); 713 714 target.predictedRank = 10000 + (controller.mPredictedSpotsCount * 100) 715 + (rank.isPresent() ? rank.getAsInt() + 1 : 0); 716 } 717 isPredictedIcon(View view)718 private static boolean isPredictedIcon(View view) { 719 return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo 720 && ((WorkspaceItemInfo) view.getTag()).container 721 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION; 722 } 723 } 724