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.LauncherAnimUtils.SCALE_PROPERTY;
19 import static com.android.launcher3.LauncherState.NORMAL;
20 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
21 import static com.android.launcher3.hybridhotseat.HotseatEduController.getSettingsIntent;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_PREDICTION_PINNED;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOTSEAT_RANKED;
24 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
25 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
26 import static com.android.launcher3.util.OnboardingPrefs.HOTSEAT_LONGPRESS_TIP_SEEN;
27 
28 import android.animation.Animator;
29 import android.animation.AnimatorSet;
30 import android.animation.ObjectAnimator;
31 import android.content.ComponentName;
32 import android.util.Log;
33 import android.view.HapticFeedbackConstants;
34 import android.view.View;
35 import android.view.ViewGroup;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.launcher3.DeviceProfile;
42 import com.android.launcher3.DragSource;
43 import com.android.launcher3.DropTarget;
44 import com.android.launcher3.Flags;
45 import com.android.launcher3.Hotseat;
46 import com.android.launcher3.LauncherPrefs;
47 import com.android.launcher3.LauncherSettings;
48 import com.android.launcher3.R;
49 import com.android.launcher3.anim.AnimationSuccessListener;
50 import com.android.launcher3.dragndrop.DragController;
51 import com.android.launcher3.dragndrop.DragOptions;
52 import com.android.launcher3.graphics.DragPreviewProvider;
53 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
54 import com.android.launcher3.logger.LauncherAtom.PredictedHotseatContainer;
55 import com.android.launcher3.logging.InstanceId;
56 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
57 import com.android.launcher3.model.data.ItemInfo;
58 import com.android.launcher3.model.data.WorkspaceItemInfo;
59 import com.android.launcher3.pm.UserCache;
60 import com.android.launcher3.popup.SystemShortcut;
61 import com.android.launcher3.testing.TestLogging;
62 import com.android.launcher3.testing.shared.TestProtocol;
63 import com.android.launcher3.touch.ItemLongClickListener;
64 import com.android.launcher3.uioverrides.PredictedAppIcon;
65 import com.android.launcher3.uioverrides.QuickstepLauncher;
66 import com.android.launcher3.views.Snackbar;
67 
68 import java.io.PrintWriter;
69 import java.util.ArrayList;
70 import java.util.Collections;
71 import java.util.List;
72 import java.util.StringJoiner;
73 import java.util.function.Predicate;
74 import java.util.stream.Collectors;
75 
76 /**
77  * Provides prediction ability for the hotseat. Fills gaps in hotseat with predicted items, allows
78  * pinning of predicted apps and manages replacement of predicted apps with user drag.
79  */
80 public class HotseatPredictionController implements DragController.DragListener,
81         SystemShortcut.Factory<QuickstepLauncher>, DeviceProfile.OnDeviceProfileChangeListener,
82         DragSource, ViewGroup.OnHierarchyChangeListener {
83 
84     private static final String TAG = "HotseatPredictionController";
85     private static final int FLAG_UPDATE_PAUSED = 1 << 0;
86     private static final int FLAG_DRAG_IN_PROGRESS = 1 << 1;
87     private static final int FLAG_FILL_IN_PROGRESS = 1 << 2;
88     private static final int FLAG_REMOVING_PREDICTED_ICON = 1 << 3;
89 
90     private int mHotSeatItemsCount;
91 
92     private QuickstepLauncher mLauncher;
93     private final Hotseat mHotseat;
94     private final Runnable mUpdateFillIfNotLoading = this::updateFillIfNotLoading;
95 
96     private List<ItemInfo> mPredictedItems = Collections.emptyList();
97 
98     private AnimatorSet mIconRemoveAnimators;
99     private int mPauseFlags = 0;
100 
101     private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
102 
103     private boolean mEnableHotseatLongPressTipForTesting = true;
104 
105     private final View.OnLongClickListener mPredictionLongClickListener = v -> {
106         if (!ItemLongClickListener.canStartDrag(mLauncher)) return false;
107         if (mLauncher.getWorkspace().isSwitchingState()) return false;
108 
109         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onWorkspaceItemLongClick");
110         if (mEnableHotseatLongPressTipForTesting && !HOTSEAT_LONGPRESS_TIP_SEEN.get(mLauncher)) {
111             Snackbar.show(mLauncher, R.string.hotseat_tip_gaps_filled,
112                     R.string.hotseat_prediction_settings, null,
113                     () -> mLauncher.startActivity(getSettingsIntent()));
114             LauncherPrefs.get(mLauncher).put(HOTSEAT_LONGPRESS_TIP_SEEN, true);
115             mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
116             return true;
117         }
118 
119         // Start the drag
120         // Use a new itemInfo so that the original predicted item is stable
121         WorkspaceItemInfo dragItem = new WorkspaceItemInfo((WorkspaceItemInfo) v.getTag());
122         v.setVisibility(View.INVISIBLE);
123         mLauncher.getWorkspace().beginDragShared(
124                 v, null, this, dragItem, new DragPreviewProvider(v), new DragOptions());
125         return true;
126     };
127 
HotseatPredictionController(QuickstepLauncher launcher)128     public HotseatPredictionController(QuickstepLauncher launcher) {
129         mLauncher = launcher;
130         mHotseat = launcher.getHotseat();
131         mHotSeatItemsCount = mLauncher.getDeviceProfile().numShownHotseatIcons;
132         mLauncher.getDragController().addDragListener(this);
133 
134         launcher.addOnDeviceProfileChangeListener(this);
135         mHotseat.getShortcutsAndWidgets().setOnHierarchyChangeListener(this);
136     }
137 
138     @Override
onChildViewAdded(View parent, View child)139     public void onChildViewAdded(View parent, View child) {
140         onHotseatHierarchyChanged();
141     }
142 
143     @Override
onChildViewRemoved(View parent, View child)144     public void onChildViewRemoved(View parent, View child) {
145         onHotseatHierarchyChanged();
146     }
147 
148     /** Enables/disabled the hotseat prediction icon long press edu for testing. */
149     @VisibleForTesting
enableHotseatEdu(boolean enable)150     public void enableHotseatEdu(boolean enable) {
151         mEnableHotseatLongPressTipForTesting = enable;
152     }
153 
onHotseatHierarchyChanged()154     private void onHotseatHierarchyChanged() {
155         if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) {
156             // Post update after a single frame to avoid layout within layout
157             MAIN_EXECUTOR.getHandler().removeCallbacks(mUpdateFillIfNotLoading);
158             MAIN_EXECUTOR.getHandler().post(mUpdateFillIfNotLoading);
159         }
160     }
161 
updateFillIfNotLoading()162     private void updateFillIfNotLoading() {
163         if (mPauseFlags == 0 && !mLauncher.isWorkspaceLoading()) {
164             fillGapsWithPrediction(true);
165         }
166     }
167 
168     /**
169      * Shows appropriate hotseat education based on prediction enabled and migration states.
170      */
showEdu()171     public void showEdu() {
172         mLauncher.getStateManager().goToState(NORMAL, true, forSuccessCallback(() -> {
173             HotseatEduController eduController = new HotseatEduController(mLauncher);
174             eduController.setPredictedApps(mPredictedItems.stream()
175                     .map(i -> (WorkspaceItemInfo) i)
176                     .collect(Collectors.toList()));
177             eduController.showEdu();
178         }));
179     }
180 
181     /**
182      * Returns if hotseat client has predictions
183      */
hasPredictions()184     public boolean hasPredictions() {
185         return !mPredictedItems.isEmpty();
186     }
187 
fillGapsWithPrediction()188     private void fillGapsWithPrediction() {
189         fillGapsWithPrediction(false);
190     }
191 
fillGapsWithPrediction(boolean animate)192     private void fillGapsWithPrediction(boolean animate) {
193         if (mPauseFlags != 0) {
194             return;
195         }
196 
197         int predictionIndex = 0;
198         int numViewsAnimated = 0;
199         ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
200         // make sure predicted icon removal and filling predictions don't step on each other
201         if (mIconRemoveAnimators != null && mIconRemoveAnimators.isRunning()) {
202             mIconRemoveAnimators.addListener(new AnimationSuccessListener() {
203                 @Override
204                 public void onAnimationSuccess(Animator animator) {
205                     fillGapsWithPrediction(animate);
206                     mIconRemoveAnimators.removeListener(this);
207                 }
208             });
209             return;
210         }
211 
212         mPauseFlags |= FLAG_FILL_IN_PROGRESS;
213         for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
214             View child = mHotseat.getChildAt(
215                     mHotseat.getCellXFromOrder(rank),
216                     mHotseat.getCellYFromOrder(rank));
217 
218             if (child != null && !isPredictedIcon(child)) {
219                 continue;
220             }
221             if (mPredictedItems.size() <= predictionIndex) {
222                 // Remove predicted apps from the past
223                 if (isPredictedIcon(child)) {
224                     mHotseat.removeView(child);
225                 }
226                 continue;
227             }
228             WorkspaceItemInfo predictedItem =
229                     (WorkspaceItemInfo) mPredictedItems.get(predictionIndex++);
230             if (isPredictedIcon(child) && child.isEnabled()) {
231                 PredictedAppIcon icon = (PredictedAppIcon) child;
232                 boolean animateIconChange = icon.shouldAnimateIconChange(predictedItem);
233                 icon.applyFromWorkspaceItem(predictedItem, animateIconChange, numViewsAnimated);
234                 if (animateIconChange) {
235                     numViewsAnimated++;
236                 }
237                 icon.finishBinding(mPredictionLongClickListener);
238             } else {
239                 newItems.add(predictedItem);
240             }
241             preparePredictionInfo(predictedItem, rank);
242         }
243         bindItems(newItems, animate);
244 
245         mPauseFlags &= ~FLAG_FILL_IN_PROGRESS;
246     }
247 
bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate)248     private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate) {
249         AnimatorSet animationSet = new AnimatorSet();
250         for (WorkspaceItemInfo item : itemsToAdd) {
251             PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
252             mLauncher.getWorkspace().addInScreenFromBind(icon, item);
253             icon.finishBinding(mPredictionLongClickListener);
254             if (animate) {
255                 animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
256             }
257         }
258         if (animate) {
259             animationSet.addListener(
260                     forSuccessCallback(this::removeOutlineDrawings));
261             animationSet.start();
262         } else {
263             removeOutlineDrawings();
264         }
265     }
266 
removeOutlineDrawings()267     private void removeOutlineDrawings() {
268         if (mOutlineDrawings.isEmpty()) return;
269         for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
270             mHotseat.removeDelegatedCellDrawing(outlineDrawing);
271         }
272         mHotseat.invalidate();
273         mOutlineDrawings.clear();
274     }
275 
276 
277     /**
278      * Unregisters callbacks and frees resources
279      */
destroy()280     public void destroy() {
281         mLauncher.removeOnDeviceProfileChangeListener(this);
282     }
283 
284     /**
285      * start and pauses predicted apps update on the hotseat
286      */
setPauseUIUpdate(boolean paused)287     public void setPauseUIUpdate(boolean paused) {
288         mPauseFlags = paused
289                 ? (mPauseFlags | FLAG_UPDATE_PAUSED)
290                 : (mPauseFlags & ~FLAG_UPDATE_PAUSED);
291         if (!paused) {
292             fillGapsWithPrediction();
293         }
294     }
295 
296     /**
297      * Ensures that if the flag FLAG_UPDATE_PAUSED is active we set it to false.
298      */
verifyUIUpdateNotPaused()299     public void verifyUIUpdateNotPaused() {
300         if ((mPauseFlags & FLAG_UPDATE_PAUSED) != 0) {
301             setPauseUIUpdate(false);
302             Log.e(TAG, "FLAG_UPDATE_PAUSED should not be set to true (see b/339700174)");
303         }
304     }
305 
306     /**
307      * Sets or updates the predicted items
308      */
setPredictedItems(FixedContainerItems items)309     public void setPredictedItems(FixedContainerItems items) {
310         mPredictedItems = new ArrayList(items.items);
311         if (mPredictedItems.isEmpty()) {
312             HotseatRestoreHelper.restoreBackup(mLauncher);
313         }
314         fillGapsWithPrediction();
315     }
316 
317     /**
318      * Pins a predicted app icon into place.
319      */
pinPrediction(ItemInfo info)320     public void pinPrediction(ItemInfo info) {
321         PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
322                 mHotseat.getCellXFromOrder(info.rank),
323                 mHotseat.getCellYFromOrder(info.rank));
324         if (icon == null) {
325             return;
326         }
327         WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo((WorkspaceItemInfo) info);
328         mLauncher.getModelWriter().addItemToDatabase(workspaceItemInfo,
329                 LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
330                 workspaceItemInfo.cellX, workspaceItemInfo.cellY);
331         ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
332         icon.pin(workspaceItemInfo);
333         mLauncher.getStatsLogManager().logger()
334                 .withItemInfo(workspaceItemInfo)
335                 .log(LAUNCHER_HOTSEAT_PREDICTION_PINNED);
336     }
337 
getPredictedIcons()338     private List<PredictedAppIcon> getPredictedIcons() {
339         List<PredictedAppIcon> icons = new ArrayList<>();
340         ViewGroup vg = mHotseat.getShortcutsAndWidgets();
341         for (int i = 0; i < vg.getChildCount(); i++) {
342             View child = vg.getChildAt(i);
343             if (isPredictedIcon(child)) {
344                 icons.add((PredictedAppIcon) child);
345             }
346         }
347         return icons;
348     }
349 
removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines, DropTarget.DragObject dragObject)350     private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines,
351             DropTarget.DragObject dragObject) {
352         if (mIconRemoveAnimators != null) {
353             mIconRemoveAnimators.end();
354         }
355         mIconRemoveAnimators = new AnimatorSet();
356         removeOutlineDrawings();
357         for (PredictedAppIcon icon : getPredictedIcons()) {
358             if (!icon.isEnabled()) {
359                 continue;
360             }
361             if (dragObject.dragSource == this && icon.equals(dragObject.originalView)) {
362                 removeIconWithoutNotify(icon);
363                 continue;
364             }
365             int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
366             outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
367                     mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
368             icon.setEnabled(false);
369             ObjectAnimator animator = ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0);
370             animator.addListener(new AnimationSuccessListener() {
371                 @Override
372                 public void onAnimationSuccess(Animator animator) {
373                     if (icon.getParent() != null) {
374                         removeIconWithoutNotify(icon);
375                     }
376                 }
377             });
378             mIconRemoveAnimators.play(animator);
379         }
380         mIconRemoveAnimators.start();
381     }
382 
383     /**
384      * Removes icon while suppressing any extra tasks performed on view-hierarchy changes.
385      * This avoids recursive/redundant updates as the control updates the UI anyway after
386      * it's animation.
387      */
removeIconWithoutNotify(PredictedAppIcon icon)388     private void removeIconWithoutNotify(PredictedAppIcon icon) {
389         mPauseFlags |= FLAG_REMOVING_PREDICTED_ICON;
390         mHotseat.removeView(icon);
391         mPauseFlags &= ~FLAG_REMOVING_PREDICTED_ICON;
392     }
393 
394     @Override
onDragStart(DropTarget.DragObject dragObject, DragOptions options)395     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
396         removePredictedApps(mOutlineDrawings, dragObject);
397         if (mOutlineDrawings.isEmpty()) return;
398         for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
399             mHotseat.addDelegatedCellDrawing(outlineDrawing);
400         }
401         mPauseFlags |= FLAG_DRAG_IN_PROGRESS;
402         mHotseat.invalidate();
403     }
404 
405     @Override
onDragEnd()406     public void onDragEnd() {
407         mPauseFlags &= ~FLAG_DRAG_IN_PROGRESS;
408         fillGapsWithPrediction(true);
409     }
410 
411     @Nullable
412     @Override
getShortcut(QuickstepLauncher activity, ItemInfo itemInfo, View originalView)413     public SystemShortcut<QuickstepLauncher> getShortcut(QuickstepLauncher activity,
414             ItemInfo itemInfo, View originalView) {
415         if (itemInfo.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
416             return null;
417         }
418         if (Flags.enablePrivateSpace() && UserCache.getInstance(
419                 activity.getApplicationContext()).getUserInfo(itemInfo.user).isPrivate()) {
420             return null;
421         }
422         return new PinPrediction(activity, itemInfo, originalView);
423     }
424 
preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank)425     private void preparePredictionInfo(WorkspaceItemInfo itemInfo, int rank) {
426         itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
427         itemInfo.rank = rank;
428         itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
429         itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
430         itemInfo.screenId = rank;
431     }
432 
433     @Override
onDeviceProfileChanged(DeviceProfile profile)434     public void onDeviceProfileChanged(DeviceProfile profile) {
435         this.mHotSeatItemsCount = profile.numShownHotseatIcons;
436     }
437 
438     @Override
onDropCompleted(View target, DropTarget.DragObject d, boolean success)439     public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) {
440         //Does nothing
441     }
442 
443     /**
444      * Logs rank info based on current list of predicted items
445      */
logLaunchedAppRankingInfo(@onNull ItemInfo itemInfo, InstanceId instanceId)446     public void logLaunchedAppRankingInfo(@NonNull ItemInfo itemInfo, InstanceId instanceId) {
447         ComponentName targetCN = itemInfo.getTargetComponent();
448         if (targetCN == null) {
449             return;
450         }
451         int rank = -1;
452         for (int i = mPredictedItems.size() - 1; i >= 0; i--) {
453             ItemInfo info = mPredictedItems.get(i);
454             if (targetCN.equals(info.getTargetComponent()) && itemInfo.user.equals(info.user)) {
455                 rank = i;
456                 break;
457             }
458         }
459         if (rank < 0) {
460             return;
461         }
462 
463         int cardinality = 0;
464         for (PredictedAppIcon icon : getPredictedIcons()) {
465             ItemInfo info = (ItemInfo) icon.getTag();
466             cardinality |= 1 << info.screenId;
467         }
468 
469         PredictedHotseatContainer.Builder containerBuilder = PredictedHotseatContainer.newBuilder();
470         containerBuilder.setCardinality(cardinality);
471         if (itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
472             containerBuilder.setIndex(rank);
473         }
474         mLauncher.getStatsLogManager().logger()
475                 .withInstanceId(instanceId)
476                 .withRank(rank)
477                 .withContainerInfo(ContainerInfo.newBuilder()
478                         .setPredictedHotseatContainer(containerBuilder)
479                         .build())
480                 .log(LAUNCHER_HOTSEAT_RANKED);
481     }
482 
483     /**
484      * Called when app/shortcut icon is removed by system. This is used to prune visible stale
485      * predictions while while waiting for AppAPrediction service to send new batch of predictions.
486      *
487      * @param matcher filter matching items that have been removed
488      */
onModelItemsRemoved(Predicate<ItemInfo> matcher)489     public void onModelItemsRemoved(Predicate<ItemInfo> matcher) {
490         if (mPredictedItems.removeIf(matcher)) {
491             fillGapsWithPrediction(true);
492         }
493     }
494 
495     /**
496      * Called when user completes adding item requiring a config activity to the hotseat
497      */
onDeferredDrop(int cellX, int cellY)498     public void onDeferredDrop(int cellX, int cellY) {
499         View child = mHotseat.getChildAt(cellX, cellY);
500         if (child instanceof PredictedAppIcon) {
501             removeIconWithoutNotify((PredictedAppIcon) child);
502         }
503     }
504 
505     private class PinPrediction extends SystemShortcut<QuickstepLauncher> {
506 
PinPrediction(QuickstepLauncher target, ItemInfo itemInfo, View originalView)507         private PinPrediction(QuickstepLauncher target, ItemInfo itemInfo, View originalView) {
508             super(R.drawable.ic_pin, R.string.pin_prediction, target,
509                     itemInfo, originalView);
510         }
511 
512         @Override
onClick(View view)513         public void onClick(View view) {
514             dismissTaskMenuView();
515             pinPrediction(mItemInfo);
516         }
517     }
518 
isPredictedIcon(View view)519     private static boolean isPredictedIcon(View view) {
520         return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
521                 && ((WorkspaceItemInfo) view.getTag()).container
522                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
523     }
524 
getStateString(int flags)525     private static String getStateString(int flags) {
526         StringJoiner str = new StringJoiner("|");
527         appendFlag(str, flags, FLAG_UPDATE_PAUSED, "FLAG_UPDATE_PAUSED");
528         appendFlag(str, flags, FLAG_DRAG_IN_PROGRESS, "FLAG_DRAG_IN_PROGRESS");
529         appendFlag(str, flags, FLAG_FILL_IN_PROGRESS, "FLAG_FILL_IN_PROGRESS");
530         appendFlag(str, flags, FLAG_REMOVING_PREDICTED_ICON,
531                 "FLAG_REMOVING_PREDICTED_ICON");
532         return str.toString();
533     }
534 
dump(String prefix, PrintWriter writer)535     public void dump(String prefix, PrintWriter writer) {
536         writer.println(prefix + "HotseatPredictionController");
537         writer.println(prefix + "\tFlags: " + getStateString(mPauseFlags));
538         writer.println(prefix + "\tmHotSeatItemsCount: " + mHotSeatItemsCount);
539         writer.println(prefix + "\tmPredictedItems: " + mPredictedItems);
540     }
541 }
542