1 /* 2 * Copyright (C) 2014 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.systemui.recents.views; 18 19 import static android.app.ActivityManager.StackId.INVALID_STACK_ID; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.animation.ValueAnimator; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.graphics.Outline; 28 import android.graphics.Point; 29 import android.graphics.Rect; 30 import android.util.AttributeSet; 31 import android.util.FloatProperty; 32 import android.util.Property; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewDebug; 36 import android.view.ViewOutlineProvider; 37 import android.widget.TextView; 38 import android.widget.Toast; 39 40 import com.android.internal.logging.MetricsLogger; 41 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 42 import com.android.systemui.Interpolators; 43 import com.android.systemui.R; 44 import com.android.systemui.recents.Recents; 45 import com.android.systemui.recents.RecentsActivity; 46 import com.android.systemui.recents.RecentsConfiguration; 47 import com.android.systemui.recents.events.EventBus; 48 import com.android.systemui.recents.events.activity.LaunchTaskEvent; 49 import com.android.systemui.recents.events.ui.DismissTaskViewEvent; 50 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; 51 import com.android.systemui.recents.events.ui.dragndrop.DragEndCancelledEvent; 52 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; 53 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; 54 import com.android.systemui.recents.misc.ReferenceCountedTrigger; 55 import com.android.systemui.recents.misc.SystemServicesProxy; 56 import com.android.systemui.recents.misc.Utilities; 57 import com.android.systemui.recents.model.Task; 58 import com.android.systemui.recents.model.TaskStack; 59 import com.android.systemui.recents.model.ThumbnailData; 60 61 import java.io.PrintWriter; 62 import java.util.ArrayList; 63 64 /** 65 * A {@link TaskView} represents a fixed view of a task. Because the TaskView's layout is directed 66 * solely by the {@link TaskStackView}, we make it a fixed size layout which allows relayouts down 67 * the view hierarchy, but not upwards from any of its children (the TaskView will relayout itself 68 * with the previous bounds if any child requests layout). 69 */ 70 public class TaskView extends FixedSizeFrameLayout implements Task.TaskCallbacks, 71 TaskStackAnimationHelper.Callbacks, View.OnClickListener, View.OnLongClickListener { 72 73 /** The TaskView callbacks */ 74 interface TaskViewCallbacks { onTaskViewClipStateChanged(TaskView tv)75 void onTaskViewClipStateChanged(TaskView tv); 76 } 77 78 /** 79 * The dim overlay is generally calculated from the task progress, but occasionally (like when 80 * launching) needs to be animated independently of the task progress. This call is only used 81 * when animating the task into Recents, when the header dim is already applied 82 */ 83 public static final Property<TaskView, Float> DIM_ALPHA_WITHOUT_HEADER = 84 new FloatProperty<TaskView>("dimAlphaWithoutHeader") { 85 @Override 86 public void setValue(TaskView tv, float dimAlpha) { 87 tv.setDimAlphaWithoutHeader(dimAlpha); 88 } 89 90 @Override 91 public Float get(TaskView tv) { 92 return tv.getDimAlpha(); 93 } 94 }; 95 96 /** 97 * The dim overlay is generally calculated from the task progress, but occasionally (like when 98 * launching) needs to be animated independently of the task progress. 99 */ 100 public static final Property<TaskView, Float> DIM_ALPHA = 101 new FloatProperty<TaskView>("dimAlpha") { 102 @Override 103 public void setValue(TaskView tv, float dimAlpha) { 104 tv.setDimAlpha(dimAlpha); 105 } 106 107 @Override 108 public Float get(TaskView tv) { 109 return tv.getDimAlpha(); 110 } 111 }; 112 113 /** 114 * The dim overlay is generally calculated from the task progress, but occasionally (like when 115 * launching) needs to be animated independently of the task progress. 116 */ 117 public static final Property<TaskView, Float> VIEW_OUTLINE_ALPHA = 118 new FloatProperty<TaskView>("viewOutlineAlpha") { 119 @Override 120 public void setValue(TaskView tv, float alpha) { 121 tv.getViewBounds().setAlpha(alpha); 122 } 123 124 @Override 125 public Float get(TaskView tv) { 126 return tv.getViewBounds().getAlpha(); 127 } 128 }; 129 130 @ViewDebug.ExportedProperty(category="recents") 131 private float mDimAlpha; 132 private float mActionButtonTranslationZ; 133 134 @ViewDebug.ExportedProperty(deepExport=true, prefix="task_") 135 private Task mTask; 136 private boolean mTaskBound; 137 @ViewDebug.ExportedProperty(category="recents") 138 private boolean mClipViewInStack = true; 139 @ViewDebug.ExportedProperty(category="recents") 140 private boolean mTouchExplorationEnabled; 141 @ViewDebug.ExportedProperty(category="recents") 142 private boolean mIsDisabledInSafeMode; 143 @ViewDebug.ExportedProperty(deepExport=true, prefix="view_bounds_") 144 private AnimateableViewBounds mViewBounds; 145 146 private AnimatorSet mTransformAnimation; 147 private ObjectAnimator mDimAnimator; 148 private ObjectAnimator mOutlineAnimator; 149 private final TaskViewTransform mTargetAnimationTransform = new TaskViewTransform(); 150 private ArrayList<Animator> mTmpAnimators = new ArrayList<>(); 151 152 @ViewDebug.ExportedProperty(deepExport=true, prefix="thumbnail_") 153 protected TaskViewThumbnail mThumbnailView; 154 @ViewDebug.ExportedProperty(deepExport=true, prefix="header_") 155 protected TaskViewHeader mHeaderView; 156 private View mActionButtonView; 157 private View mIncompatibleAppToastView; 158 private TaskViewCallbacks mCb; 159 160 @ViewDebug.ExportedProperty(category="recents") 161 private Point mDownTouchPos = new Point(); 162 163 private Toast mDisabledAppToast; 164 TaskView(Context context)165 public TaskView(Context context) { 166 this(context, null); 167 } 168 TaskView(Context context, AttributeSet attrs)169 public TaskView(Context context, AttributeSet attrs) { 170 this(context, attrs, 0); 171 } 172 TaskView(Context context, AttributeSet attrs, int defStyleAttr)173 public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { 174 this(context, attrs, defStyleAttr, 0); 175 } 176 TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)177 public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 178 super(context, attrs, defStyleAttr, defStyleRes); 179 RecentsConfiguration config = Recents.getConfiguration(); 180 Resources res = context.getResources(); 181 mViewBounds = createOutlineProvider(); 182 if (config.fakeShadows) { 183 setBackground(new FakeShadowDrawable(res, config)); 184 } 185 setOutlineProvider(mViewBounds); 186 setOnLongClickListener(this); 187 setAccessibilityDelegate(new TaskViewAccessibilityDelegate(this)); 188 } 189 190 /** Set callback */ setCallbacks(TaskViewCallbacks cb)191 void setCallbacks(TaskViewCallbacks cb) { 192 mCb = cb; 193 } 194 195 /** 196 * Called from RecentsActivity when it is relaunched. 197 */ onReload(boolean isResumingFromVisible)198 void onReload(boolean isResumingFromVisible) { 199 if (!Recents.getSystemServices().hasFreeformWorkspaceSupport()) { 200 resetNoUserInteractionState(); 201 } 202 if (!isResumingFromVisible) { 203 resetViewProperties(); 204 } 205 } 206 207 /** Gets the task */ getTask()208 public Task getTask() { 209 return mTask; 210 } 211 212 /* Create an outline provider to clip and outline the view */ createOutlineProvider()213 protected AnimateableViewBounds createOutlineProvider() { 214 return new AnimateableViewBounds(this, mContext.getResources().getDimensionPixelSize( 215 R.dimen.recents_task_view_shadow_rounded_corners_radius)); 216 } 217 218 /** Returns the view bounds. */ getViewBounds()219 AnimateableViewBounds getViewBounds() { 220 return mViewBounds; 221 } 222 223 @Override onFinishInflate()224 protected void onFinishInflate() { 225 // Bind the views 226 mHeaderView = findViewById(R.id.task_view_bar); 227 mThumbnailView = findViewById(R.id.task_view_thumbnail); 228 mThumbnailView.updateClipToTaskBar(mHeaderView); 229 mActionButtonView = findViewById(R.id.lock_to_app_fab); 230 mActionButtonView.setOutlineProvider(new ViewOutlineProvider() { 231 @Override 232 public void getOutline(View view, Outline outline) { 233 // Set the outline to match the FAB background 234 outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight()); 235 outline.setAlpha(0.35f); 236 } 237 }); 238 mActionButtonView.setOnClickListener(this); 239 mActionButtonTranslationZ = mActionButtonView.getTranslationZ(); 240 } 241 242 /** 243 * Update the task view when the configuration changes. 244 */ onConfigurationChanged()245 protected void onConfigurationChanged() { 246 mHeaderView.onConfigurationChanged(); 247 } 248 249 @Override onSizeChanged(int w, int h, int oldw, int oldh)250 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 251 super.onSizeChanged(w, h, oldw, oldh); 252 if (w > 0 && h > 0) { 253 mHeaderView.onTaskViewSizeChanged(w, h); 254 mThumbnailView.onTaskViewSizeChanged(w, h); 255 256 mActionButtonView.setTranslationX(w - getMeasuredWidth()); 257 mActionButtonView.setTranslationY(h - getMeasuredHeight()); 258 } 259 } 260 261 @Override hasOverlappingRendering()262 public boolean hasOverlappingRendering() { 263 return false; 264 } 265 266 @Override onInterceptTouchEvent(MotionEvent ev)267 public boolean onInterceptTouchEvent(MotionEvent ev) { 268 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 269 mDownTouchPos.set((int) (ev.getX() * getScaleX()), (int) (ev.getY() * getScaleY())); 270 } 271 return super.onInterceptTouchEvent(ev); 272 } 273 274 @Override measureContents(int width, int height)275 protected void measureContents(int width, int height) { 276 int widthWithoutPadding = width - mPaddingLeft - mPaddingRight; 277 int heightWithoutPadding = height - mPaddingTop - mPaddingBottom; 278 int widthSpec = MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY); 279 int heightSpec = MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY); 280 281 // Measure the content 282 measureChildren(widthSpec, heightSpec); 283 284 setMeasuredDimension(width, height); 285 } 286 updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback)287 void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, 288 AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) { 289 RecentsConfiguration config = Recents.getConfiguration(); 290 cancelTransformAnimation(); 291 292 // Compose the animations for the transform 293 mTmpAnimators.clear(); 294 toTransform.applyToTaskView(this, mTmpAnimators, toAnimation, !config.fakeShadows); 295 if (toAnimation.isImmediate()) { 296 if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) { 297 setDimAlpha(toTransform.dimAlpha); 298 } 299 if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) { 300 mViewBounds.setAlpha(toTransform.viewOutlineAlpha); 301 } 302 // Manually call back to the animator listener and update callback 303 if (toAnimation.getListener() != null) { 304 toAnimation.getListener().onAnimationEnd(null); 305 } 306 if (updateCallback != null) { 307 updateCallback.onAnimationUpdate(null); 308 } 309 } else { 310 // Both the progress and the update are a function of the bounds movement of the task 311 if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) { 312 mDimAnimator = ObjectAnimator.ofFloat(this, DIM_ALPHA, getDimAlpha(), 313 toTransform.dimAlpha); 314 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mDimAnimator)); 315 } 316 if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) { 317 mOutlineAnimator = ObjectAnimator.ofFloat(this, VIEW_OUTLINE_ALPHA, 318 mViewBounds.getAlpha(), toTransform.viewOutlineAlpha); 319 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mOutlineAnimator)); 320 } 321 if (updateCallback != null) { 322 ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1); 323 updateCallbackAnim.addUpdateListener(updateCallback); 324 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, updateCallbackAnim)); 325 } 326 327 // Create the animator 328 mTransformAnimation = toAnimation.createAnimator(mTmpAnimators); 329 mTransformAnimation.start(); 330 mTargetAnimationTransform.copyFrom(toTransform); 331 } 332 } 333 334 /** Resets this view's properties */ resetViewProperties()335 void resetViewProperties() { 336 cancelTransformAnimation(); 337 setDimAlpha(0); 338 setVisibility(View.VISIBLE); 339 getViewBounds().reset(); 340 getHeaderView().reset(); 341 TaskViewTransform.reset(this); 342 343 mActionButtonView.setScaleX(1f); 344 mActionButtonView.setScaleY(1f); 345 mActionButtonView.setAlpha(0f); 346 mActionButtonView.setTranslationX(0f); 347 mActionButtonView.setTranslationY(0f); 348 mActionButtonView.setTranslationZ(mActionButtonTranslationZ); 349 if (mIncompatibleAppToastView != null) { 350 mIncompatibleAppToastView.setVisibility(View.INVISIBLE); 351 } 352 } 353 354 /** 355 * @return whether we are animating towards {@param transform} 356 */ isAnimatingTo(TaskViewTransform transform)357 boolean isAnimatingTo(TaskViewTransform transform) { 358 return mTransformAnimation != null && mTransformAnimation.isStarted() 359 && mTargetAnimationTransform.isSame(transform); 360 } 361 362 /** 363 * Cancels any current transform animations. 364 */ cancelTransformAnimation()365 public void cancelTransformAnimation() { 366 cancelDimAnimationIfExists(); 367 Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation); 368 Utilities.cancelAnimationWithoutCallbacks(mOutlineAnimator); 369 } 370 cancelDimAnimationIfExists()371 private void cancelDimAnimationIfExists() { 372 if (mDimAnimator != null) { 373 mDimAnimator.cancel(); 374 } 375 } 376 377 /** Enables/disables handling touch on this task view. */ setTouchEnabled(boolean enabled)378 public void setTouchEnabled(boolean enabled) { 379 setOnClickListener(enabled ? this : null); 380 } 381 382 /** Animates this task view if the user does not interact with the stack after a certain time. */ startNoUserInteractionAnimation()383 public void startNoUserInteractionAnimation() { 384 mHeaderView.startNoUserInteractionAnimation(); 385 } 386 387 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ setNoUserInteractionState()388 void setNoUserInteractionState() { 389 mHeaderView.setNoUserInteractionState(); 390 } 391 392 /** Resets the state tracking that the user has not interacted with the stack after a certain time. */ resetNoUserInteractionState()393 void resetNoUserInteractionState() { 394 mHeaderView.resetNoUserInteractionState(); 395 } 396 397 /** Dismisses this task. */ dismissTask()398 void dismissTask() { 399 // Animate out the view and call the callback 400 final TaskView tv = this; 401 DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv); 402 dismissEvent.addPostAnimationCallback(new Runnable() { 403 @Override 404 public void run() { 405 EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv, 406 new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION, 407 Interpolators.FAST_OUT_SLOW_IN))); 408 } 409 }); 410 EventBus.getDefault().send(dismissEvent); 411 } 412 413 /** 414 * Returns whether this view should be clipped, or any views below should clip against this 415 * view. 416 */ shouldClipViewInStack()417 boolean shouldClipViewInStack() { 418 // Never clip for freeform tasks or if invisible 419 if (mTask.isFreeformTask() || getVisibility() != View.VISIBLE) { 420 return false; 421 } 422 return mClipViewInStack; 423 } 424 425 /** Sets whether this view should be clipped, or clipped against. */ setClipViewInStack(boolean clip)426 void setClipViewInStack(boolean clip) { 427 if (clip != mClipViewInStack) { 428 mClipViewInStack = clip; 429 if (mCb != null) { 430 mCb.onTaskViewClipStateChanged(this); 431 } 432 } 433 } 434 getHeaderView()435 public TaskViewHeader getHeaderView() { 436 return mHeaderView; 437 } 438 439 /** 440 * Sets the current dim. 441 */ setDimAlpha(float dimAlpha)442 public void setDimAlpha(float dimAlpha) { 443 mDimAlpha = dimAlpha; 444 mThumbnailView.setDimAlpha(dimAlpha); 445 mHeaderView.setDimAlpha(dimAlpha); 446 } 447 448 /** 449 * Sets the current dim without updating the header's dim. 450 */ setDimAlphaWithoutHeader(float dimAlpha)451 public void setDimAlphaWithoutHeader(float dimAlpha) { 452 mDimAlpha = dimAlpha; 453 mThumbnailView.setDimAlpha(dimAlpha); 454 } 455 456 /** 457 * Returns the current dim. 458 */ getDimAlpha()459 public float getDimAlpha() { 460 return mDimAlpha; 461 } 462 463 /** 464 * Explicitly sets the focused state of this task. 465 */ setFocusedState(boolean isFocused, boolean requestViewFocus)466 public void setFocusedState(boolean isFocused, boolean requestViewFocus) { 467 if (isFocused) { 468 if (requestViewFocus && !isFocused()) { 469 requestFocus(); 470 } 471 } else { 472 if (isAccessibilityFocused() && mTouchExplorationEnabled) { 473 clearAccessibilityFocus(); 474 } 475 } 476 } 477 478 /** 479 * Shows the action button. 480 * @param fadeIn whether or not to animate the action button in. 481 * @param fadeInDuration the duration of the action button animation, only used if 482 * {@param fadeIn} is true. 483 */ showActionButton(boolean fadeIn, int fadeInDuration)484 public void showActionButton(boolean fadeIn, int fadeInDuration) { 485 mActionButtonView.setVisibility(View.VISIBLE); 486 487 if (fadeIn && mActionButtonView.getAlpha() < 1f) { 488 mActionButtonView.animate() 489 .alpha(1f) 490 .scaleX(1f) 491 .scaleY(1f) 492 .setDuration(fadeInDuration) 493 .setInterpolator(Interpolators.ALPHA_IN) 494 .start(); 495 } else { 496 mActionButtonView.setScaleX(1f); 497 mActionButtonView.setScaleY(1f); 498 mActionButtonView.setAlpha(1f); 499 mActionButtonView.setTranslationZ(mActionButtonTranslationZ); 500 } 501 } 502 503 /** 504 * Immediately hides the action button. 505 * 506 * @param fadeOut whether or not to animate the action button out. 507 */ hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, final Animator.AnimatorListener animListener)508 public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, 509 final Animator.AnimatorListener animListener) { 510 if (fadeOut && mActionButtonView.getAlpha() > 0f) { 511 if (scaleDown) { 512 float toScale = 0.9f; 513 mActionButtonView.animate() 514 .scaleX(toScale) 515 .scaleY(toScale); 516 } 517 mActionButtonView.animate() 518 .alpha(0f) 519 .setDuration(fadeOutDuration) 520 .setInterpolator(Interpolators.ALPHA_OUT) 521 .withEndAction(new Runnable() { 522 @Override 523 public void run() { 524 if (animListener != null) { 525 animListener.onAnimationEnd(null); 526 } 527 mActionButtonView.setVisibility(View.INVISIBLE); 528 } 529 }) 530 .start(); 531 } else { 532 mActionButtonView.setAlpha(0f); 533 mActionButtonView.setVisibility(View.INVISIBLE); 534 if (animListener != null) { 535 animListener.onAnimationEnd(null); 536 } 537 } 538 } 539 540 /**** TaskStackAnimationHelper.Callbacks Implementation ****/ 541 542 @Override onPrepareLaunchTargetForEnterAnimation()543 public void onPrepareLaunchTargetForEnterAnimation() { 544 // These values will be animated in when onStartLaunchTargetEnterAnimation() is called 545 setDimAlphaWithoutHeader(0); 546 mActionButtonView.setAlpha(0f); 547 if (mIncompatibleAppToastView != null && 548 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) { 549 mIncompatibleAppToastView.setAlpha(0f); 550 } 551 } 552 553 @Override onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger)554 public void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, 555 boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger) { 556 cancelDimAnimationIfExists(); 557 558 // Dim the view after the app window transitions down into recents 559 postAnimationTrigger.increment(); 560 AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT); 561 mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this, 562 DIM_ALPHA_WITHOUT_HEADER, getDimAlpha(), transform.dimAlpha)); 563 mDimAnimator.addListener(postAnimationTrigger.decrementOnAnimationEnd()); 564 mDimAnimator.start(); 565 566 if (screenPinningEnabled) { 567 showActionButton(true /* fadeIn */, duration /* fadeInDuration */); 568 } 569 570 if (mIncompatibleAppToastView != null && 571 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) { 572 mIncompatibleAppToastView.animate() 573 .alpha(1f) 574 .setDuration(duration) 575 .setInterpolator(Interpolators.ALPHA_IN) 576 .start(); 577 } 578 } 579 580 @Override onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, ReferenceCountedTrigger postAnimationTrigger)581 public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, 582 ReferenceCountedTrigger postAnimationTrigger) { 583 Utilities.cancelAnimationWithoutCallbacks(mDimAnimator); 584 585 // Un-dim the view before/while launching the target 586 AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT); 587 mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this, 588 DIM_ALPHA, getDimAlpha(), 0)); 589 mDimAnimator.start(); 590 591 postAnimationTrigger.increment(); 592 hideActionButton(true /* fadeOut */, duration, 593 !screenPinningRequested /* scaleDown */, 594 postAnimationTrigger.decrementOnAnimationEnd()); 595 } 596 597 @Override onStartFrontTaskEnterAnimation(boolean screenPinningEnabled)598 public void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled) { 599 if (screenPinningEnabled) { 600 showActionButton(false /* fadeIn */, 0 /* fadeInDuration */); 601 } 602 } 603 604 /**** TaskCallbacks Implementation ****/ 605 onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation, Rect displayRect)606 public void onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation, 607 Rect displayRect) { 608 SystemServicesProxy ssp = Recents.getSystemServices(); 609 mTouchExplorationEnabled = touchExplorationEnabled; 610 mTask = t; 611 mTaskBound = true; 612 mTask.addCallback(this); 613 mIsDisabledInSafeMode = !mTask.isSystemApp && ssp.isInSafeMode(); 614 mThumbnailView.bindToTask(mTask, mIsDisabledInSafeMode, displayOrientation, displayRect); 615 mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode); 616 617 if (!t.isDockable && ssp.hasDockedTask()) { 618 if (mIncompatibleAppToastView == null) { 619 mIncompatibleAppToastView = Utilities.findViewStubById(this, 620 R.id.incompatible_app_toast_stub).inflate(); 621 TextView msg = findViewById(com.android.internal.R.id.message); 622 msg.setText(R.string.dock_non_resizeble_failed_to_dock_text); 623 } 624 mIncompatibleAppToastView.setVisibility(View.VISIBLE); 625 } else if (mIncompatibleAppToastView != null) { 626 mIncompatibleAppToastView.setVisibility(View.INVISIBLE); 627 } 628 } 629 630 @Override onTaskDataLoaded(Task task, ThumbnailData thumbnailData)631 public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) { 632 if (mTaskBound) { 633 // Update each of the views to the new task data 634 mThumbnailView.onTaskDataLoaded(thumbnailData); 635 mHeaderView.onTaskDataLoaded(); 636 } 637 } 638 639 @Override onTaskDataUnloaded()640 public void onTaskDataUnloaded() { 641 // Unbind each of the views from the task and remove the task callback 642 mTask.removeCallback(this); 643 mThumbnailView.unbindFromTask(); 644 mHeaderView.unbindFromTask(mTouchExplorationEnabled); 645 mTaskBound = false; 646 } 647 648 @Override onTaskStackIdChanged()649 public void onTaskStackIdChanged() { 650 // Force rebind the header, the thumbnail does not change due to stack changes 651 mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode); 652 mHeaderView.onTaskDataLoaded(); 653 } 654 655 /**** View.OnClickListener Implementation ****/ 656 657 @Override onClick(final View v)658 public void onClick(final View v) { 659 if (mIsDisabledInSafeMode) { 660 Context context = getContext(); 661 String msg = context.getString(R.string.recents_launch_disabled_message, mTask.title); 662 if (mDisabledAppToast != null) { 663 mDisabledAppToast.cancel(); 664 } 665 mDisabledAppToast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); 666 mDisabledAppToast.show(); 667 return; 668 } 669 670 boolean screenPinningRequested = false; 671 if (v == mActionButtonView) { 672 // Reset the translation of the action button before we animate it out 673 mActionButtonView.setTranslationZ(0f); 674 screenPinningRequested = true; 675 } 676 EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, INVALID_STACK_ID, 677 screenPinningRequested)); 678 679 MetricsLogger.action(v.getContext(), MetricsEvent.ACTION_OVERVIEW_SELECT, 680 mTask.key.getComponent().toString()); 681 } 682 683 /**** View.OnLongClickListener Implementation ****/ 684 685 @Override onLongClick(View v)686 public boolean onLongClick(View v) { 687 SystemServicesProxy ssp = Recents.getSystemServices(); 688 boolean inBounds = false; 689 Rect clipBounds = new Rect(mViewBounds.mClipBounds); 690 if (!clipBounds.isEmpty()) { 691 // If we are clipping the view to the bounds, manually do the hit test. 692 clipBounds.scale(getScaleX()); 693 inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y); 694 } else { 695 // Otherwise just make sure we're within the view's bounds. 696 inBounds = mDownTouchPos.x <= getWidth() && mDownTouchPos.y <= getHeight(); 697 } 698 if (v == this && inBounds && !ssp.hasDockedTask()) { 699 // Start listening for drag events 700 setClipViewInStack(false); 701 702 mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2; 703 mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2; 704 705 EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1); 706 EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos)); 707 return true; 708 } 709 return false; 710 } 711 712 /**** Events ****/ 713 onBusEvent(DragEndEvent event)714 public final void onBusEvent(DragEndEvent event) { 715 if (!(event.dropTarget instanceof TaskStack.DockState)) { 716 event.addPostAnimationCallback(() -> { 717 // Reset the clip state for the drag view after the end animation completes 718 setClipViewInStack(true); 719 }); 720 } 721 EventBus.getDefault().unregister(this); 722 } 723 onBusEvent(DragEndCancelledEvent event)724 public final void onBusEvent(DragEndCancelledEvent event) { 725 // Reset the clip state for the drag view after the cancel animation completes 726 event.addPostAnimationCallback(() -> { 727 setClipViewInStack(true); 728 }); 729 } 730 dump(String prefix, PrintWriter writer)731 public void dump(String prefix, PrintWriter writer) { 732 String innerPrefix = prefix + " "; 733 734 writer.print(prefix); writer.print("TaskView"); 735 writer.print(" mTask="); writer.print(mTask.key.id); 736 writer.println(); 737 738 mThumbnailView.dump(innerPrefix, writer); 739 } 740 } 741