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