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