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