1 /* 2 * Copyright (C) 2017 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 17 package com.android.quickstep.views; 18 19 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; 20 import static com.android.launcher3.anim.Interpolators.ACCEL; 21 import static com.android.launcher3.anim.Interpolators.ACCEL_2; 22 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 23 import static com.android.launcher3.anim.Interpolators.LINEAR; 24 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; 25 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorSet; 29 import android.animation.ObjectAnimator; 30 import android.animation.TimeInterpolator; 31 import android.animation.ValueAnimator; 32 import android.annotation.TargetApi; 33 import android.app.ActivityManager; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.graphics.Canvas; 38 import android.graphics.Point; 39 import android.graphics.Rect; 40 import android.graphics.drawable.Drawable; 41 import android.os.Build; 42 import android.os.Bundle; 43 import android.os.Handler; 44 import android.os.UserHandle; 45 import android.support.annotation.Nullable; 46 import android.text.Layout; 47 import android.text.StaticLayout; 48 import android.text.TextPaint; 49 import android.util.ArraySet; 50 import android.util.AttributeSet; 51 import android.util.SparseBooleanArray; 52 import android.view.KeyEvent; 53 import android.view.LayoutInflater; 54 import android.view.MotionEvent; 55 import android.view.View; 56 import android.view.ViewDebug; 57 import android.view.accessibility.AccessibilityEvent; 58 import android.view.accessibility.AccessibilityNodeInfo; 59 import android.widget.ListView; 60 61 import com.android.launcher3.BaseActivity; 62 import com.android.launcher3.DeviceProfile; 63 import com.android.launcher3.Insettable; 64 import com.android.launcher3.PagedView; 65 import com.android.launcher3.R; 66 import com.android.launcher3.Utilities; 67 import com.android.launcher3.anim.AnimatorPlaybackController; 68 import com.android.launcher3.anim.PropertyListBuilder; 69 import com.android.launcher3.config.FeatureFlags; 70 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; 71 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; 72 import com.android.launcher3.util.PendingAnimation; 73 import com.android.launcher3.util.Themes; 74 import com.android.quickstep.OverviewCallbacks; 75 import com.android.quickstep.QuickScrubController; 76 import com.android.quickstep.RecentsModel; 77 import com.android.quickstep.TaskUtils; 78 import com.android.quickstep.util.ClipAnimationHelper; 79 import com.android.quickstep.util.TaskViewDrawable; 80 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; 81 import com.android.systemui.shared.recents.model.RecentsTaskLoader; 82 import com.android.systemui.shared.recents.model.Task; 83 import com.android.systemui.shared.recents.model.TaskStack; 84 import com.android.systemui.shared.recents.model.ThumbnailData; 85 import com.android.systemui.shared.system.ActivityManagerWrapper; 86 import com.android.systemui.shared.system.BackgroundExecutor; 87 import com.android.systemui.shared.system.PackageManagerWrapper; 88 import com.android.systemui.shared.system.TaskStackChangeListener; 89 90 import java.util.ArrayList; 91 import java.util.function.Consumer; 92 93 /** 94 * A list of recent tasks. 95 */ 96 @TargetApi(Build.VERSION_CODES.P) 97 public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable { 98 99 private static final String TAG = RecentsView.class.getSimpleName(); 100 101 private final Rect mTempRect = new Rect(); 102 103 private static final int DISMISS_TASK_DURATION = 300; 104 // The threshold at which we update the SystemUI flags when animating from the task into the app 105 private static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.6f; 106 107 private static final float[] sTempFloatArray = new float[3]; 108 109 protected final T mActivity; 110 private final QuickScrubController mQuickScrubController; 111 private final float mFastFlingVelocity; 112 private final RecentsModel mModel; 113 private final int mTaskTopMargin; 114 115 private final ScrollState mScrollState = new ScrollState(); 116 // Keeps track of the previously known visible tasks for purposes of loading/unloading task data 117 private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); 118 119 private boolean mIsClearAllButtonFullyRevealed; 120 121 /** 122 * TODO: Call reloadIdNeeded in onTaskStackChanged. 123 */ 124 private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { 125 @Override 126 public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { 127 if (!mHandleTaskStackChanges) { 128 return; 129 } 130 updateThumbnail(taskId, snapshot); 131 } 132 133 @Override 134 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { 135 if (!mHandleTaskStackChanges) { 136 return; 137 } 138 // Check this is for the right user 139 if (!checkCurrentOrManagedUserId(userId, getContext())) { 140 return; 141 } 142 143 // Remove the task immediately from the task list 144 TaskView taskView = getTaskView(taskId); 145 if (taskView != null) { 146 removeView(taskView); 147 } 148 } 149 150 @Override 151 public void onActivityUnpinned() { 152 if (!mHandleTaskStackChanges) { 153 return; 154 } 155 // TODO: Re-enable layout transitions for addition of the unpinned task 156 reloadIfNeeded(); 157 } 158 159 @Override 160 public void onTaskRemoved(int taskId) { 161 if (!mHandleTaskStackChanges) { 162 return; 163 } 164 BackgroundExecutor.get().submit(() -> { 165 TaskView taskView = getTaskView(taskId); 166 if (taskView == null) { 167 return; 168 } 169 Handler handler = taskView.getHandler(); 170 if (handler == null) { 171 return; 172 } 173 174 // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and 175 // remove all these checks 176 Task.TaskKey taskKey = taskView.getTask().key; 177 if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), 178 taskKey.userId) == null) { 179 // The package was uninstalled 180 handler.post(() -> 181 dismissTask(taskView, true /* animate */, false /* removeTask */)); 182 } else { 183 RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext()); 184 RecentsTaskLoadPlan.PreloadOptions opts = 185 new RecentsTaskLoadPlan.PreloadOptions(); 186 opts.loadTitles = false; 187 loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1, 188 UserHandle.myUserId()); 189 if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) { 190 // The task was removed from the recents list 191 handler.post(() -> 192 dismissTask(taskView, true /* animate */, false /* removeTask */)); 193 } 194 } 195 }); 196 } 197 198 @Override 199 public void onPinnedStackAnimationStarted() { 200 // Needed for activities that auto-enter PiP, which will not trigger a remote 201 // animation to be created 202 mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); 203 } 204 }; 205 206 private int mLoadPlanId = -1; 207 208 // Only valid until the launcher state changes to NORMAL 209 private int mRunningTaskId = -1; 210 private boolean mRunningTaskTileHidden; 211 private Task mTmpRunningTask; 212 213 private boolean mRunningTaskIconScaledDown = false; 214 215 private boolean mOverviewStateEnabled; 216 private boolean mHandleTaskStackChanges; 217 private Runnable mNextPageSwitchRunnable; 218 private boolean mSwipeDownShouldLaunchApp; 219 220 private PendingAnimation mPendingAnimation; 221 222 @ViewDebug.ExportedProperty(category = "launcher") 223 private float mContentAlpha = 1; 224 225 // Keeps track of task views whose visual state should not be reset 226 private ArraySet<TaskView> mIgnoreResetTaskViews = new ArraySet<>(); 227 228 private View mClearAllButton; 229 230 // Variables for empty state 231 private final Drawable mEmptyIcon; 232 private final CharSequence mEmptyMessage; 233 private final TextPaint mEmptyMessagePaint; 234 private final Point mLastMeasureSize = new Point(); 235 private final int mEmptyMessagePadding; 236 private boolean mShowEmptyMessage; 237 private Layout mEmptyTextLayout; 238 239 private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = 240 (inMultiWindowMode) -> { 241 if (!inMultiWindowMode && mOverviewStateEnabled) { 242 // TODO: Re-enable layout transitions for addition of the unpinned task 243 reloadIfNeeded(); 244 } 245 }; 246 247 public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { 248 super(context, attrs, defStyleAttr); 249 setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); 250 enableFreeScroll(true); 251 setClipToOutline(true); 252 253 mFastFlingVelocity = getResources() 254 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity); 255 mActivity = (T) BaseActivity.fromContext(context); 256 mQuickScrubController = new QuickScrubController(mActivity, this); 257 mModel = RecentsModel.getInstance(context); 258 259 mIsRtl = !Utilities.isRtl(getResources()); 260 setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); 261 mTaskTopMargin = getResources() 262 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); 263 264 mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); 265 mEmptyIcon.setCallback(this); 266 mEmptyMessage = context.getText(R.string.recents_empty_message); 267 mEmptyMessagePaint = new TextPaint(); 268 mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); 269 mEmptyMessagePaint.setTextSize(getResources() 270 .getDimension(R.dimen.recents_empty_message_text_size)); 271 mEmptyMessagePadding = getResources() 272 .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); 273 setWillNotDraw(false); 274 updateEmptyMessage(); 275 setFocusable(false); 276 } 277 278 public boolean isRtl() { 279 return mIsRtl; 280 } 281 282 public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { 283 TaskView taskView = getTaskView(taskId); 284 if (taskView != null) { 285 taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData); 286 } 287 return taskView; 288 } 289 290 @Override 291 protected void onWindowVisibilityChanged(int visibility) { 292 super.onWindowVisibilityChanged(visibility); 293 updateTaskStackListenerState(); 294 } 295 296 @Override 297 protected void onAttachedToWindow() { 298 super.onAttachedToWindow(); 299 updateTaskStackListenerState(); 300 mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); 301 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); 302 } 303 304 @Override 305 protected void onDetachedFromWindow() { 306 super.onDetachedFromWindow(); 307 updateTaskStackListenerState(); 308 mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); 309 ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); 310 } 311 312 @Override 313 public void onViewRemoved(View child) { 314 super.onViewRemoved(child); 315 316 // Clear the task data for the removed child if it was visible 317 Task task = ((TaskView) child).getTask(); 318 if (mHasVisibleTaskData.get(task.key.id)) { 319 mHasVisibleTaskData.delete(task.key.id); 320 RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); 321 loader.unloadTaskData(task); 322 loader.getHighResThumbnailLoader().onTaskInvisible(task); 323 } 324 onChildViewsChanged(); 325 } 326 327 public boolean isTaskViewVisible(TaskView tv) { 328 // For now, just check if it's the active task or an adjacent task 329 return Math.abs(indexOfChild(tv) - getNextPage()) <= 1; 330 } 331 332 public TaskView getTaskView(int taskId) { 333 for (int i = 0; i < getChildCount(); i++) { 334 TaskView tv = (TaskView) getChildAt(i); 335 if (tv.getTask().key.id == taskId) { 336 return tv; 337 } 338 } 339 return null; 340 } 341 342 public void setOverviewStateEnabled(boolean enabled) { 343 mOverviewStateEnabled = enabled; 344 updateTaskStackListenerState(); 345 } 346 347 public void setNextPageSwitchRunnable(Runnable r) { 348 mNextPageSwitchRunnable = r; 349 } 350 351 @Override 352 protected void onPageEndTransition() { 353 super.onPageEndTransition(); 354 if (mNextPageSwitchRunnable != null) { 355 mNextPageSwitchRunnable.run(); 356 mNextPageSwitchRunnable = null; 357 } 358 if (getNextPage() > 0) { 359 setSwipeDownShouldLaunchApp(true); 360 } 361 } 362 363 private int getScrollEnd() { 364 return mIsRtl ? 0 : mMaxScrollX; 365 } 366 367 private float calculateClearAllButtonAlpha() { 368 final int childCount = getChildCount(); 369 if (mShowEmptyMessage || childCount == 0 || mPageScrolls == null 370 || childCount != mPageScrolls.length) { 371 return 0; 372 } 373 374 final int scrollEnd = getScrollEnd(); 375 final int oldestChildScroll = getScrollForPage(childCount - 1); 376 377 final int clearAllButtonMotionRange = scrollEnd - oldestChildScroll; 378 if (clearAllButtonMotionRange == 0) return 0; 379 380 final float alphaUnbound = ((float) (getScrollX() - oldestChildScroll)) / 381 clearAllButtonMotionRange; 382 if (alphaUnbound > 1) return 0; 383 384 return Math.max(alphaUnbound, 0); 385 } 386 387 private void updateClearAllButtonAlpha() { 388 if (mClearAllButton != null) { 389 final float alpha = calculateClearAllButtonAlpha(); 390 final boolean revealed = alpha == 1; 391 if (mIsClearAllButtonFullyRevealed != revealed) { 392 mIsClearAllButtonFullyRevealed = revealed; 393 mClearAllButton.setImportantForAccessibility(revealed ? 394 IMPORTANT_FOR_ACCESSIBILITY_YES : 395 IMPORTANT_FOR_ACCESSIBILITY_NO); 396 } 397 mClearAllButton.setAlpha(alpha * mContentAlpha); 398 } 399 } 400 401 @Override 402 protected void onScrollChanged(int l, int t, int oldl, int oldt) { 403 super.onScrollChanged(l, t, oldl, oldt); 404 updateClearAllButtonAlpha(); 405 } 406 407 @Override 408 protected void restoreScrollOnLayout() { 409 if (mIsClearAllButtonFullyRevealed) { 410 scrollAndForceFinish(getScrollEnd()); 411 } else { 412 super.restoreScrollOnLayout(); 413 } 414 } 415 416 @Override 417 public boolean onTouchEvent(MotionEvent ev) { 418 if (ev.getAction() == MotionEvent.ACTION_DOWN && mTouchState == TOUCH_STATE_REST 419 && mScroller.isFinished() && mIsClearAllButtonFullyRevealed) { 420 mClearAllButton.getHitRect(mTempRect); 421 mTempRect.offset(-getLeft(), -getTop()); 422 if (mTempRect.contains((int) ev.getX(), (int) ev.getY())) { 423 // If nothing is in motion, let the Clear All button process the event. 424 return false; 425 } 426 } 427 428 if (ev.getAction() == MotionEvent.ACTION_UP && mShowEmptyMessage) { 429 onAllTasksRemoved(); 430 } 431 return super.onTouchEvent(ev); 432 } 433 434 private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) { 435 if (mPendingAnimation != null) { 436 mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(loadPlan)); 437 return; 438 } 439 TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null; 440 if (stack == null) { 441 removeAllViews(); 442 onTaskStackUpdated(); 443 return; 444 } 445 446 int oldChildCount = getChildCount(); 447 448 // Ensure there are as many views as there are tasks in the stack (adding and trimming as 449 // necessary) 450 final LayoutInflater inflater = LayoutInflater.from(getContext()); 451 final ArrayList<Task> tasks = new ArrayList<>(stack.getTasks()); 452 453 final int requiredChildCount = tasks.size(); 454 for (int i = getChildCount(); i < requiredChildCount; i++) { 455 final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false); 456 addView(taskView); 457 } 458 while (getChildCount() > requiredChildCount) { 459 final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1); 460 removeView(taskView); 461 } 462 463 // Unload existing visible task data 464 unloadVisibleTaskData(); 465 466 // Rebind and reset all task views 467 for (int i = requiredChildCount - 1; i >= 0; i--) { 468 final int pageIndex = requiredChildCount - i - 1; 469 final Task task = tasks.get(i); 470 final TaskView taskView = (TaskView) getChildAt(pageIndex); 471 taskView.bind(task); 472 } 473 resetTaskVisuals(); 474 475 if (oldChildCount != getChildCount()) { 476 mQuickScrubController.snapToNextTaskIfAvailable(); 477 } 478 onTaskStackUpdated(); 479 } 480 481 protected void onTaskStackUpdated() { } 482 483 public void resetTaskVisuals() { 484 for (int i = getChildCount() - 1; i >= 0; i--) { 485 TaskView taskView = (TaskView) getChildAt(i); 486 if (!mIgnoreResetTaskViews.contains(taskView)) { 487 taskView.resetVisualProperties(); 488 } 489 } 490 if (mRunningTaskTileHidden) { 491 setRunningTaskHidden(mRunningTaskTileHidden); 492 } 493 applyIconScale(false /* animate */); 494 495 updateCurveProperties(); 496 // Update the set of visible task's data 497 loadVisibleTaskData(); 498 } 499 500 private void updateTaskStackListenerState() { 501 boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() 502 && getWindowVisibility() == VISIBLE; 503 if (handleTaskStackChanges != mHandleTaskStackChanges) { 504 mHandleTaskStackChanges = handleTaskStackChanges; 505 if (handleTaskStackChanges) { 506 reloadIfNeeded(); 507 } 508 } 509 } 510 511 @Override 512 public void setInsets(Rect insets) { 513 mInsets.set(insets); 514 DeviceProfile dp = mActivity.getDeviceProfile(); 515 getTaskSize(dp, mTempRect); 516 517 // Keep this logic in sync with ActivityControlHelper.getTranslationYForQuickScrub. 518 mTempRect.top -= mTaskTopMargin; 519 setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, 520 dp.availableWidthPx + mInsets.left - mTempRect.right, 521 dp.availableHeightPx + mInsets.top - mTempRect.bottom); 522 } 523 524 protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); 525 526 public void getTaskSize(Rect outRect) { 527 getTaskSize(mActivity.getDeviceProfile(), outRect); 528 } 529 530 @Override 531 protected boolean computeScrollHelper() { 532 boolean scrolling = super.computeScrollHelper(); 533 boolean isFlingingFast = false; 534 updateCurveProperties(); 535 if (scrolling || (mTouchState == TOUCH_STATE_SCROLLING)) { 536 if (scrolling) { 537 // Check if we are flinging quickly to disable high res thumbnail loading 538 isFlingingFast = mScroller.getCurrVelocity() > mFastFlingVelocity; 539 } 540 541 // After scrolling, update the visible task's data 542 loadVisibleTaskData(); 543 } 544 545 // Update the high res thumbnail loader 546 RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); 547 loader.getHighResThumbnailLoader().setFlingingFast(isFlingingFast); 548 return scrolling; 549 } 550 551 /** 552 * Scales and adjusts translation of adjacent pages as if on a curved carousel. 553 */ 554 public void updateCurveProperties() { 555 if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) { 556 return; 557 } 558 final int halfPageWidth = getNormalChildWidth() / 2; 559 final int screenCenter = mInsets.left + getPaddingLeft() + getScrollX() + halfPageWidth; 560 final int halfScreenWidth = getMeasuredWidth() / 2; 561 final int pageSpacing = mPageSpacing; 562 563 final int pageCount = getPageCount(); 564 for (int i = 0; i < pageCount; i++) { 565 View page = getPageAt(i); 566 float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; 567 float distanceFromScreenCenter = screenCenter - pageCenter; 568 float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; 569 mScrollState.linearInterpolation = Math.min(1, 570 Math.abs(distanceFromScreenCenter) / distanceToReachEdge); 571 ((PageCallbacks) page).onPageScroll(mScrollState); 572 } 573 } 574 575 /** 576 * Iterates through all thet asks, and loads the associated task data for newly visible tasks, 577 * and unloads the associated task data for tasks that are no longer visible. 578 */ 579 public void loadVisibleTaskData() { 580 if (!mOverviewStateEnabled) { 581 // Skip loading visible task data if we've already left the overview state 582 return; 583 } 584 585 RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); 586 int centerPageIndex = getPageNearestToCenterOfScreen(); 587 int lower = Math.max(0, centerPageIndex - 2); 588 int upper = Math.min(centerPageIndex + 2, getChildCount() - 1); 589 int numChildren = getChildCount(); 590 591 // Update the task data for the in/visible children 592 for (int i = 0; i < numChildren; i++) { 593 TaskView taskView = (TaskView) getChildAt(i); 594 Task task = taskView.getTask(); 595 boolean visible = lower <= i && i <= upper; 596 if (visible) { 597 if (task == mTmpRunningTask) { 598 // Skip loading if this is the task that we are animating into 599 continue; 600 } 601 if (!mHasVisibleTaskData.get(task.key.id)) { 602 loader.loadTaskData(task); 603 loader.getHighResThumbnailLoader().onTaskVisible(task); 604 } 605 mHasVisibleTaskData.put(task.key.id, visible); 606 } else { 607 if (mHasVisibleTaskData.get(task.key.id)) { 608 loader.unloadTaskData(task); 609 loader.getHighResThumbnailLoader().onTaskInvisible(task); 610 } 611 mHasVisibleTaskData.delete(task.key.id); 612 } 613 } 614 } 615 616 /** 617 * Unloads any associated data from the currently visible tasks 618 */ 619 private void unloadVisibleTaskData() { 620 RecentsTaskLoader loader = mModel.getRecentsTaskLoader(); 621 for (int i = 0; i < mHasVisibleTaskData.size(); i++) { 622 if (mHasVisibleTaskData.valueAt(i)) { 623 TaskView taskView = getTaskView(mHasVisibleTaskData.keyAt(i)); 624 Task task = taskView.getTask(); 625 loader.unloadTaskData(task); 626 loader.getHighResThumbnailLoader().onTaskInvisible(task); 627 } 628 } 629 mHasVisibleTaskData.clear(); 630 } 631 632 protected abstract void onAllTasksRemoved(); 633 634 public void reset() { 635 mRunningTaskId = -1; 636 mRunningTaskTileHidden = false; 637 638 unloadVisibleTaskData(); 639 setCurrentPage(0); 640 641 OverviewCallbacks.get(getContext()).onResetOverview(); 642 } 643 644 /** 645 * Reloads the view if anything in recents changed. 646 */ 647 public void reloadIfNeeded() { 648 if (!mModel.isLoadPlanValid(mLoadPlanId)) { 649 mLoadPlanId = mModel.loadTasks(mRunningTaskId, this::applyLoadPlan); 650 } 651 } 652 653 /** 654 * Ensures that the first task in the view represents {@param task} and reloads the view 655 * if needed. This allows the swipe-up gesture to assume that the first tile always 656 * corresponds to the correct task. 657 * All subsequent calls to reload will keep the task as the first item until {@link #reset()} 658 * is called. 659 * Also scrolls the view to this task 660 */ 661 public void showTask(int runningTaskId) { 662 if (getChildCount() == 0) { 663 // Add an empty view for now until the task plan is loaded and applied 664 final TaskView taskView = (TaskView) LayoutInflater.from(getContext()) 665 .inflate(R.layout.task, this, false); 666 addView(taskView); 667 668 // The temporary running task is only used for the duration between the start of the 669 // gesture and the task list is loaded and applied 670 mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(), 0, 0), null, 671 null, "", "", 0, 0, false, true, false, false, 672 new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false); 673 taskView.bind(mTmpRunningTask); 674 } 675 setCurrentTask(runningTaskId); 676 } 677 678 /** 679 * Hides the tile associated with {@link #mRunningTaskId} 680 */ 681 public void setRunningTaskHidden(boolean isHidden) { 682 mRunningTaskTileHidden = isHidden; 683 TaskView runningTask = getTaskView(mRunningTaskId); 684 if (runningTask != null) { 685 runningTask.setAlpha(isHidden ? 0 : mContentAlpha); 686 } 687 } 688 689 /** 690 * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile. 691 */ 692 public void setCurrentTask(int runningTaskId) { 693 boolean runningTaskTileHidden = mRunningTaskTileHidden; 694 boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown; 695 696 setRunningTaskIconScaledDown(false, false); 697 setRunningTaskHidden(false); 698 mRunningTaskId = runningTaskId; 699 setRunningTaskIconScaledDown(runningTaskIconScaledDown, false); 700 setRunningTaskHidden(runningTaskTileHidden); 701 702 setCurrentPage(0); 703 704 // Load the tasks (if the loading is already 705 mLoadPlanId = mModel.loadTasks(runningTaskId, this::applyLoadPlan); 706 } 707 708 public void showNextTask() { 709 TaskView runningTaskView = getTaskView(mRunningTaskId); 710 if (runningTaskView == null) { 711 // Launch the first task 712 if (getChildCount() > 0) { 713 ((TaskView) getChildAt(0)).launchTask(true /* animate */); 714 } 715 } else { 716 // Get the next launch task 717 int runningTaskIndex = indexOfChild(runningTaskView); 718 int nextTaskIndex = Math.max(0, Math.min(getChildCount() - 1, runningTaskIndex + 1)); 719 if (nextTaskIndex < getChildCount()) { 720 ((TaskView) getChildAt(nextTaskIndex)).launchTask(true /* animate */); 721 } 722 } 723 } 724 725 public QuickScrubController getQuickScrubController() { 726 return mQuickScrubController; 727 } 728 729 public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) { 730 if (mRunningTaskIconScaledDown == isScaledDown) { 731 return; 732 } 733 mRunningTaskIconScaledDown = isScaledDown; 734 applyIconScale(animate); 735 } 736 737 private void applyIconScale(boolean animate) { 738 float scale = mRunningTaskIconScaledDown ? 0 : 1; 739 TaskView firstTask = getTaskView(mRunningTaskId); 740 if (firstTask != null) { 741 if (animate) { 742 firstTask.animateIconToScaleAndDim(scale); 743 } else { 744 firstTask.setIconScaleAndDim(scale); 745 } 746 } 747 } 748 749 public void setSwipeDownShouldLaunchApp(boolean swipeDownShouldLaunchApp) { 750 mSwipeDownShouldLaunchApp = swipeDownShouldLaunchApp; 751 } 752 753 public boolean shouldSwipeDownLaunchApp() { 754 return mSwipeDownShouldLaunchApp; 755 } 756 757 public interface PageCallbacks { 758 759 /** 760 * Updates the page UI based on scroll params. 761 */ 762 default void onPageScroll(ScrollState scrollState) {}; 763 } 764 765 public static class ScrollState { 766 767 /** 768 * The progress from 0 to 1, where 0 is the center 769 * of the screen and 1 is the edge of the screen. 770 */ 771 public float linearInterpolation; 772 } 773 774 public void addIgnoreResetTask(TaskView taskView) { 775 mIgnoreResetTaskViews.add(taskView); 776 } 777 778 public void removeIgnoreResetTask(TaskView taskView) { 779 mIgnoreResetTaskViews.remove(taskView); 780 } 781 782 private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { 783 addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); 784 addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), 785 duration, LINEAR, anim); 786 } 787 788 private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, 789 boolean shouldLog) { 790 if (task != null) { 791 ActivityManagerWrapper.getInstance().removeTask(task.key.id); 792 if (shouldLog) { 793 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( 794 onEndListener.logAction, Direction.UP, index, 795 TaskUtils.getComponentKeyForTask(task.key)); 796 } 797 } 798 } 799 800 public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, 801 boolean shouldRemoveTask, long duration) { 802 if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { 803 throw new IllegalStateException("Another pending animation is still running"); 804 } 805 AnimatorSet anim = new AnimatorSet(); 806 PendingAnimation pendingAnimation = new PendingAnimation(anim); 807 808 int count = getChildCount(); 809 if (count == 0) { 810 return pendingAnimation; 811 } 812 813 int[] oldScroll = new int[count]; 814 getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); 815 816 int[] newScroll = new int[count]; 817 getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); 818 819 int scrollDiffPerPage = 0; 820 int leftmostPage = mIsRtl ? count -1 : 0; 821 int rightmostPage = mIsRtl ? 0 : count - 1; 822 if (count > 1) { 823 int secondRightmostPage = mIsRtl ? 1 : count - 2; 824 scrollDiffPerPage = oldScroll[rightmostPage] - oldScroll[secondRightmostPage]; 825 } 826 int draggedIndex = indexOfChild(taskView); 827 828 boolean needsCurveUpdates = false; 829 for (int i = 0; i < count; i++) { 830 View child = getChildAt(i); 831 if (child == taskView) { 832 if (animateTaskView) { 833 addDismissedTaskAnimations(taskView, anim, duration); 834 } 835 } else { 836 // If we just take newScroll - oldScroll, everything to the right of dragged task 837 // translates to the left. We need to offset this in some cases: 838 // - In RTL, add page offset to all pages, since we want pages to move to the right 839 // Additionally, add a page offset if: 840 // - Current page is rightmost page (leftmost for RTL) 841 // - Dragging an adjacent page on the left side (right side for RTL) 842 int offset = mIsRtl ? scrollDiffPerPage : 0; 843 if (mCurrentPage == draggedIndex) { 844 int lastPage = mIsRtl ? leftmostPage : rightmostPage; 845 if (mCurrentPage == lastPage) { 846 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; 847 } 848 } else { 849 // Dragging an adjacent page. 850 int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) 851 if (draggedIndex == negativeAdjacent) { 852 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; 853 } 854 } 855 int scrollDiff = newScroll[i] - oldScroll[i] + offset; 856 if (scrollDiff != 0) { 857 addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), 858 duration, ACCEL, anim); 859 needsCurveUpdates = true; 860 } 861 } 862 } 863 864 if (needsCurveUpdates) { 865 ValueAnimator va = ValueAnimator.ofFloat(0, 1); 866 va.addUpdateListener((a) -> updateCurveProperties()); 867 anim.play(va); 868 } 869 870 // Add a tiny bit of translation Z, so that it draws on top of other views 871 if (animateTaskView) { 872 taskView.setTranslationZ(0.1f); 873 } 874 875 mPendingAnimation = pendingAnimation; 876 mPendingAnimation.addEndListener((onEndListener) -> { 877 if (onEndListener.isSuccess) { 878 if (shouldRemoveTask) { 879 removeTask(taskView.getTask(), draggedIndex, onEndListener, true); 880 } 881 int pageToSnapTo = mCurrentPage; 882 if (draggedIndex < pageToSnapTo) { 883 pageToSnapTo -= 1; 884 } 885 removeView(taskView); 886 if (getChildCount() == 0) { 887 onAllTasksRemoved(); 888 } else if (!mIsClearAllButtonFullyRevealed) { 889 snapToPageImmediately(pageToSnapTo); 890 } 891 } 892 resetTaskVisuals(); 893 mPendingAnimation = null; 894 }); 895 return pendingAnimation; 896 } 897 898 public PendingAnimation createAllTasksDismissAnimation(long duration) { 899 if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { 900 throw new IllegalStateException("Another pending animation is still running"); 901 } 902 AnimatorSet anim = new AnimatorSet(); 903 PendingAnimation pendingAnimation = new PendingAnimation(anim); 904 905 int count = getChildCount(); 906 for (int i = 0; i < count; i++) { 907 addDismissedTaskAnimations(getChildAt(i), anim, duration); 908 } 909 910 mPendingAnimation = pendingAnimation; 911 mPendingAnimation.addEndListener((onEndListener) -> { 912 if (onEndListener.isSuccess) { 913 while (getChildCount() != 0) { 914 TaskView taskView = getPageAt(getChildCount() - 1); 915 removeTask(taskView.getTask(), -1, onEndListener, false); 916 removeView(taskView); 917 } 918 onAllTasksRemoved(); 919 } 920 mPendingAnimation = null; 921 }); 922 return pendingAnimation; 923 } 924 925 private static void addAnim(ObjectAnimator anim, long duration, 926 TimeInterpolator interpolator, AnimatorSet set) { 927 anim.setDuration(duration).setInterpolator(interpolator); 928 set.play(anim); 929 } 930 931 private boolean snapToPageRelative(int delta, boolean cycle) { 932 if (getPageCount() == 0) { 933 return false; 934 } 935 final int newPageUnbound = getNextPage() + delta; 936 if (!cycle && (newPageUnbound < 0 || newPageUnbound >= getChildCount())) { 937 return false; 938 } 939 snapToPage((newPageUnbound + getPageCount()) % getPageCount()); 940 return true; 941 } 942 943 private void runDismissAnimation(PendingAnimation pendingAnim) { 944 AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( 945 pendingAnim.anim, DISMISS_TASK_DURATION); 946 controller.dispatchOnStart(); 947 controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE)); 948 controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); 949 controller.start(); 950 } 951 952 public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { 953 runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, 954 DISMISS_TASK_DURATION)); 955 } 956 957 public void dismissAllTasks() { 958 runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); 959 } 960 961 @Override 962 public boolean dispatchKeyEvent(KeyEvent event) { 963 if (event.getAction() == KeyEvent.ACTION_DOWN) { 964 switch (event.getKeyCode()) { 965 case KeyEvent.KEYCODE_TAB: 966 return snapToPageRelative(event.isShiftPressed() ? -1 : 1, 967 event.isAltPressed() /* cycle */); 968 case KeyEvent.KEYCODE_DPAD_RIGHT: 969 return snapToPageRelative(mIsRtl ? -1 : 1, false /* cycle */); 970 case KeyEvent.KEYCODE_DPAD_LEFT: 971 return snapToPageRelative(mIsRtl ? 1 : -1, false /* cycle */); 972 case KeyEvent.KEYCODE_DEL: 973 case KeyEvent.KEYCODE_FORWARD_DEL: 974 dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/, 975 true /*removeTask*/); 976 return true; 977 case KeyEvent.KEYCODE_NUMPAD_DOT: 978 if (event.isAltPressed()) { 979 // Numpad DEL pressed while holding Alt. 980 dismissTask((TaskView) getChildAt(getNextPage()), true /*animateTaskView*/, 981 true /*removeTask*/); 982 return true; 983 } 984 } 985 } 986 return super.dispatchKeyEvent(event); 987 } 988 989 @Override 990 protected void onFocusChanged(boolean gainFocus, int direction, 991 @Nullable Rect previouslyFocusedRect) { 992 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 993 if (gainFocus && getChildCount() > 0) { 994 switch (direction) { 995 case FOCUS_FORWARD: 996 setCurrentPage(0); 997 break; 998 case FOCUS_BACKWARD: 999 case FOCUS_RIGHT: 1000 case FOCUS_LEFT: 1001 setCurrentPage(getChildCount() - 1); 1002 break; 1003 } 1004 } 1005 } 1006 1007 public float getContentAlpha() { 1008 return mContentAlpha; 1009 } 1010 1011 public void setContentAlpha(float alpha) { 1012 alpha = Utilities.boundToRange(alpha, 0, 1); 1013 mContentAlpha = alpha; 1014 for (int i = getChildCount() - 1; i >= 0; i--) { 1015 TaskView child = getPageAt(i); 1016 if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { 1017 getChildAt(i).setAlpha(alpha); 1018 } 1019 } 1020 1021 int alphaInt = Math.round(alpha * 255); 1022 mEmptyMessagePaint.setAlpha(alphaInt); 1023 mEmptyIcon.setAlpha(alphaInt); 1024 updateClearAllButtonAlpha(); 1025 } 1026 1027 private float[] getAdjacentScaleAndTranslation(TaskView currTask, 1028 float currTaskToScale, float currTaskToTranslationY) { 1029 float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale()); 1030 sTempFloatArray[0] = currTaskToScale; 1031 sTempFloatArray[1] = mIsRtl ? -displacement : displacement; 1032 sTempFloatArray[2] = currTaskToTranslationY; 1033 return sTempFloatArray; 1034 } 1035 1036 @Override 1037 public void onViewAdded(View child) { 1038 super.onViewAdded(child); 1039 child.setAlpha(mContentAlpha); 1040 onChildViewsChanged(); 1041 } 1042 1043 @Override 1044 public TaskView getPageAt(int index) { 1045 return (TaskView) getChildAt(index); 1046 } 1047 1048 public void updateEmptyMessage() { 1049 boolean isEmpty = getChildCount() == 0; 1050 boolean hasSizeChanged = mLastMeasureSize.x != getWidth() 1051 || mLastMeasureSize.y != getHeight(); 1052 if (isEmpty == mShowEmptyMessage && !hasSizeChanged) { 1053 return; 1054 } 1055 setContentDescription(isEmpty ? mEmptyMessage : ""); 1056 mShowEmptyMessage = isEmpty; 1057 updateEmptyStateUi(hasSizeChanged); 1058 invalidate(); 1059 } 1060 1061 @Override 1062 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1063 super.onLayout(changed, left, top, right, bottom); 1064 updateEmptyStateUi(changed); 1065 1066 // Set the pivot points to match the task preview center 1067 setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin) 1068 + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2); 1069 setPivotX(((mInsets.left + getPaddingLeft()) 1070 + (getWidth() - mInsets.right - getPaddingRight())) / 2); 1071 } 1072 1073 private void updateEmptyStateUi(boolean sizeChanged) { 1074 boolean hasValidSize = getWidth() > 0 && getHeight() > 0; 1075 if (sizeChanged && hasValidSize) { 1076 mEmptyTextLayout = null; 1077 mLastMeasureSize.set(getWidth(), getHeight()); 1078 } 1079 updateClearAllButtonAlpha(); 1080 1081 if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { 1082 int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; 1083 mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), 1084 mEmptyMessagePaint, availableWidth) 1085 .setAlignment(Layout.Alignment.ALIGN_CENTER) 1086 .build(); 1087 int totalHeight = mEmptyTextLayout.getHeight() 1088 + mEmptyMessagePadding + mEmptyIcon.getIntrinsicHeight(); 1089 1090 int top = (mLastMeasureSize.y - totalHeight) / 2; 1091 int left = (mLastMeasureSize.x - mEmptyIcon.getIntrinsicWidth()) / 2; 1092 mEmptyIcon.setBounds(left, top, left + mEmptyIcon.getIntrinsicWidth(), 1093 top + mEmptyIcon.getIntrinsicHeight()); 1094 } 1095 } 1096 1097 @Override 1098 protected boolean verifyDrawable(Drawable who) { 1099 return super.verifyDrawable(who) || (mShowEmptyMessage && who == mEmptyIcon); 1100 } 1101 1102 protected void maybeDrawEmptyMessage(Canvas canvas) { 1103 if (mShowEmptyMessage && mEmptyTextLayout != null) { 1104 // Offset to center in the visible (non-padded) part of RecentsView 1105 mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), 1106 mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); 1107 canvas.save(); 1108 canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, 1109 (mTempRect.top - mTempRect.bottom) / 2); 1110 mEmptyIcon.draw(canvas); 1111 canvas.translate(mEmptyMessagePadding, 1112 mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); 1113 mEmptyTextLayout.draw(canvas); 1114 canvas.restore(); 1115 } 1116 } 1117 1118 /** 1119 * Animate adjacent tasks off screen while scaling up. 1120 * 1121 * If launching one of the adjacent tasks, parallax the center task and other adjacent task 1122 * to the right. 1123 */ 1124 public AnimatorSet createAdjacentPageAnimForTaskLaunch( 1125 TaskView tv, ClipAnimationHelper clipAnimationHelper) { 1126 AnimatorSet anim = new AnimatorSet(); 1127 1128 int taskIndex = indexOfChild(tv); 1129 int centerTaskIndex = getCurrentPage(); 1130 boolean launchingCenterTask = taskIndex == centerTaskIndex; 1131 1132 float toScale = clipAnimationHelper.getSourceRect().width() 1133 / clipAnimationHelper.getTargetRect().width(); 1134 float toTranslationY = clipAnimationHelper.getSourceRect().centerY() 1135 - clipAnimationHelper.getTargetRect().centerY(); 1136 if (launchingCenterTask) { 1137 TaskView centerTask = getPageAt(centerTaskIndex); 1138 if (taskIndex - 1 >= 0) { 1139 TaskView adjacentTask = getPageAt(taskIndex - 1); 1140 float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, 1141 toScale, toTranslationY); 1142 scaleAndTranslation[1] = -scaleAndTranslation[1]; 1143 anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); 1144 } 1145 if (taskIndex + 1 < getPageCount()) { 1146 TaskView adjacentTask = getPageAt(taskIndex + 1); 1147 float[] scaleAndTranslation = getAdjacentScaleAndTranslation(centerTask, 1148 toScale, toTranslationY); 1149 anim.play(createAnimForChild(adjacentTask, scaleAndTranslation)); 1150 } 1151 } else { 1152 // We are launching an adjacent task, so parallax the center and other adjacent task. 1153 float displacementX = tv.getWidth() * (toScale - tv.getCurveScale()); 1154 anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X, 1155 mIsRtl ? -displacementX : displacementX)); 1156 1157 int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex); 1158 if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) { 1159 anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex), 1160 new PropertyListBuilder() 1161 .translationX(mIsRtl ? -displacementX : displacementX) 1162 .scale(1) 1163 .build())); 1164 } 1165 } 1166 return anim; 1167 } 1168 1169 private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) { 1170 AnimatorSet anim = new AnimatorSet(); 1171 anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0])); 1172 anim.play(ObjectAnimator.ofPropertyValuesHolder(child, 1173 new PropertyListBuilder() 1174 .translationX(toScaleAndTranslation[1]) 1175 .translationY(toScaleAndTranslation[2]) 1176 .build())); 1177 return anim; 1178 } 1179 1180 public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { 1181 if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { 1182 throw new IllegalStateException("Another pending animation is still running"); 1183 } 1184 1185 int count = getChildCount(); 1186 if (count == 0) { 1187 return new PendingAnimation(new AnimatorSet()); 1188 } 1189 1190 tv.setVisibility(INVISIBLE); 1191 int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); 1192 TaskViewDrawable drawable = new TaskViewDrawable(tv, this); 1193 getOverlay().add(drawable); 1194 1195 ObjectAnimator drawableAnim = 1196 ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0); 1197 drawableAnim.setInterpolator(LINEAR); 1198 drawableAnim.addUpdateListener((animator) -> { 1199 // Once we pass a certain threshold, update the sysui flags to match the target tasks' 1200 // flags 1201 mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, 1202 animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD 1203 ? targetSysUiFlags 1204 : 0); 1205 }); 1206 1207 AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, 1208 drawable.getClipAnimationHelper()); 1209 anim.play(drawableAnim); 1210 anim.setDuration(duration); 1211 1212 Consumer<Boolean> onTaskLaunchFinish = (result) -> { 1213 onTaskLaunched(result); 1214 tv.setVisibility(VISIBLE); 1215 getOverlay().remove(drawable); 1216 }; 1217 1218 mPendingAnimation = new PendingAnimation(anim); 1219 mPendingAnimation.addEndListener((onEndListener) -> { 1220 if (onEndListener.isSuccess) { 1221 Consumer<Boolean> onLaunchResult = (result) -> { 1222 onTaskLaunchFinish.accept(result); 1223 if (!result) { 1224 tv.notifyTaskLaunchFailed(TAG); 1225 } 1226 }; 1227 tv.launchTask(false, onLaunchResult, getHandler()); 1228 Task task = tv.getTask(); 1229 if (task != null) { 1230 mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( 1231 onEndListener.logAction, Direction.DOWN, indexOfChild(tv), 1232 TaskUtils.getComponentKeyForTask(task.key)); 1233 } 1234 } else { 1235 onTaskLaunchFinish.accept(false); 1236 } 1237 mPendingAnimation = null; 1238 }); 1239 return mPendingAnimation; 1240 } 1241 1242 public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); 1243 1244 protected void onTaskLaunched(boolean success) { 1245 resetTaskVisuals(); 1246 } 1247 1248 @Override 1249 protected void notifyPageSwitchListener(int prevPage) { 1250 super.notifyPageSwitchListener(prevPage); 1251 loadVisibleTaskData(); 1252 } 1253 1254 @Override 1255 protected String getCurrentPageDescription() { 1256 return ""; 1257 } 1258 1259 private int additionalScrollForClearAllButton() { 1260 return (int) getResources().getDimension( 1261 R.dimen.clear_all_container_width) - getPaddingEnd(); 1262 } 1263 1264 @Override 1265 protected int computeMaxScrollX() { 1266 if (getChildCount() == 0) { 1267 return super.computeMaxScrollX(); 1268 } 1269 1270 // Allow a clear_all_container_width-sized gap after the last task. 1271 return super.computeMaxScrollX() + (mIsRtl ? 0 : additionalScrollForClearAllButton()); 1272 } 1273 1274 @Override 1275 protected int offsetForPageScrolls() { 1276 return mIsRtl ? additionalScrollForClearAllButton() : 0; 1277 } 1278 1279 public void setClearAllButton(View clearAllButton) { 1280 mClearAllButton = clearAllButton; 1281 updateClearAllButtonAlpha(); 1282 } 1283 1284 private void onChildViewsChanged() { 1285 final int childCount = getChildCount(); 1286 mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE); 1287 setFocusable(childCount != 0); 1288 } 1289 1290 public void revealClearAllButton() { 1291 setCurrentPage(getChildCount() - 1); // Loads tasks info if needed. 1292 scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0); 1293 } 1294 1295 @Override 1296 public boolean performAccessibilityAction(int action, Bundle arguments) { 1297 if (getChildCount() > 0) { 1298 switch (action) { 1299 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 1300 if (!mIsClearAllButtonFullyRevealed && getCurrentPage() == getPageCount() - 1) { 1301 revealClearAllButton(); 1302 return true; 1303 } 1304 } 1305 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 1306 if (mIsClearAllButtonFullyRevealed) { 1307 setCurrentPage(getChildCount() - 1); 1308 return true; 1309 } 1310 } 1311 break; 1312 } 1313 } 1314 return super.performAccessibilityAction(action, arguments); 1315 } 1316 1317 @Override 1318 public void addChildrenForAccessibility(ArrayList<View> outChildren) { 1319 outChildren.add(mClearAllButton); 1320 for (int i = getChildCount() - 1; i >= 0; --i) { 1321 outChildren.add(getChildAt(i)); 1322 } 1323 } 1324 1325 @Override 1326 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 1327 super.onInitializeAccessibilityNodeInfo(info); 1328 1329 if (getChildCount() > 0) { 1330 info.addAction(mIsClearAllButtonFullyRevealed ? 1331 AccessibilityNodeInfo.ACTION_SCROLL_FORWARD : 1332 AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 1333 info.setScrollable(true); 1334 } 1335 1336 final AccessibilityNodeInfo.CollectionInfo 1337 collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( 1338 1, getChildCount(), false, 1339 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_NONE); 1340 info.setCollectionInfo(collectionInfo); 1341 } 1342 1343 @Override 1344 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 1345 super.onInitializeAccessibilityEvent(event); 1346 1347 event.setScrollable(getPageCount() > 0); 1348 1349 if (!mIsClearAllButtonFullyRevealed 1350 && event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { 1351 final int childCount = getChildCount(); 1352 final int[] visibleTasks = getVisibleChildrenRange(); 1353 event.setFromIndex(childCount - visibleTasks[1] - 1); 1354 event.setToIndex(childCount - visibleTasks[0] - 1); 1355 event.setItemCount(childCount); 1356 } 1357 } 1358 1359 @Override 1360 public CharSequence getAccessibilityClassName() { 1361 // To hear position-in-list related feedback from Talkback. 1362 return ListView.class.getName(); 1363 } 1364 1365 @Override 1366 protected boolean isPageOrderFlipped() { 1367 return true; 1368 } 1369 1370 public boolean performTaskAccessibilityActionExtra(int action) { 1371 return false; 1372 } 1373 } 1374