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