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