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.app.ActivityManager; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Outline; 29 import android.graphics.Point; 30 import android.graphics.Rect; 31 import android.util.AttributeSet; 32 import android.util.FloatProperty; 33 import android.util.Property; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.ViewDebug; 37 import android.view.ViewOutlineProvider; 38 import android.widget.TextView; 39 import android.widget.Toast; 40 41 import com.android.internal.logging.MetricsLogger; 42 import com.android.internal.logging.MetricsProto.MetricsEvent; 43 import com.android.systemui.Interpolators; 44 import com.android.systemui.R; 45 import com.android.systemui.recents.Recents; 46 import com.android.systemui.recents.RecentsActivity; 47 import com.android.systemui.recents.RecentsConfiguration; 48 import com.android.systemui.recents.events.EventBus; 49 import com.android.systemui.recents.events.activity.LaunchTaskEvent; 50 import com.android.systemui.recents.events.ui.DismissTaskViewEvent; 51 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; 52 import com.android.systemui.recents.events.ui.dragndrop.DragEndCancelledEvent; 53 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent; 54 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent; 55 import com.android.systemui.recents.misc.ReferenceCountedTrigger; 56 import com.android.systemui.recents.misc.SystemServicesProxy; 57 import com.android.systemui.recents.misc.Utilities; 58 import com.android.systemui.recents.model.Task; 59 import com.android.systemui.recents.model.TaskStack; 60 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 @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 TaskViewThumbnail mThumbnailView; 152 @ViewDebug.ExportedProperty(deepExport=true, prefix="header_") 153 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 = Recents.getConfiguration(); 178 Resources res = context.getResources(); 179 mViewBounds = new AnimateableViewBounds(this, res.getDimensionPixelSize( 180 R.dimen.recents_task_view_shadow_rounded_corners_radius)); 181 if (config.fakeShadows) { 182 setBackground(new FakeShadowDrawable(res, config)); 183 } 184 setOutlineProvider(mViewBounds); 185 setOnLongClickListener(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 /** Returns the view bounds. */ getViewBounds()209 AnimateableViewBounds getViewBounds() { 210 return mViewBounds; 211 } 212 213 @Override onFinishInflate()214 protected void onFinishInflate() { 215 // Bind the views 216 mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar); 217 mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail); 218 mThumbnailView.updateClipToTaskBar(mHeaderView); 219 mActionButtonView = findViewById(R.id.lock_to_app_fab); 220 mActionButtonView.setOutlineProvider(new ViewOutlineProvider() { 221 @Override 222 public void getOutline(View view, Outline outline) { 223 // Set the outline to match the FAB background 224 outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight()); 225 outline.setAlpha(0.35f); 226 } 227 }); 228 mActionButtonView.setOnClickListener(this); 229 mActionButtonTranslationZ = mActionButtonView.getTranslationZ(); 230 } 231 232 /** 233 * Update the task view when the configuration changes. 234 */ onConfigurationChanged()235 void onConfigurationChanged() { 236 mHeaderView.onConfigurationChanged(); 237 } 238 239 @Override onSizeChanged(int w, int h, int oldw, int oldh)240 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 241 super.onSizeChanged(w, h, oldw, oldh); 242 if (w > 0 && h > 0) { 243 mHeaderView.onTaskViewSizeChanged(w, h); 244 mThumbnailView.onTaskViewSizeChanged(w, h); 245 246 mActionButtonView.setTranslationX(w - getMeasuredWidth()); 247 mActionButtonView.setTranslationY(h - getMeasuredHeight()); 248 } 249 } 250 251 @Override hasOverlappingRendering()252 public boolean hasOverlappingRendering() { 253 return false; 254 } 255 256 @Override onInterceptTouchEvent(MotionEvent ev)257 public boolean onInterceptTouchEvent(MotionEvent ev) { 258 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 259 mDownTouchPos.set((int) (ev.getX() * getScaleX()), (int) (ev.getY() * getScaleY())); 260 } 261 return super.onInterceptTouchEvent(ev); 262 } 263 264 265 @Override measureContents(int width, int height)266 protected void measureContents(int width, int height) { 267 int widthWithoutPadding = width - mPaddingLeft - mPaddingRight; 268 int heightWithoutPadding = height - mPaddingTop - mPaddingBottom; 269 int widthSpec = MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY); 270 int heightSpec = MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.EXACTLY); 271 272 // Measure the content 273 measureChildren(widthSpec, heightSpec); 274 275 setMeasuredDimension(width, height); 276 } 277 updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback)278 void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, 279 AnimationProps toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) { 280 RecentsConfiguration config = Recents.getConfiguration(); 281 cancelTransformAnimation(); 282 283 // Compose the animations for the transform 284 mTmpAnimators.clear(); 285 toTransform.applyToTaskView(this, mTmpAnimators, toAnimation, !config.fakeShadows); 286 if (toAnimation.isImmediate()) { 287 if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) { 288 setDimAlpha(toTransform.dimAlpha); 289 } 290 if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) { 291 mViewBounds.setAlpha(toTransform.viewOutlineAlpha); 292 } 293 // Manually call back to the animator listener and update callback 294 if (toAnimation.getListener() != null) { 295 toAnimation.getListener().onAnimationEnd(null); 296 } 297 if (updateCallback != null) { 298 updateCallback.onAnimationUpdate(null); 299 } 300 } else { 301 // Both the progress and the update are a function of the bounds movement of the task 302 if (Float.compare(getDimAlpha(), toTransform.dimAlpha) != 0) { 303 mDimAnimator = ObjectAnimator.ofFloat(this, DIM_ALPHA, getDimAlpha(), 304 toTransform.dimAlpha); 305 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mDimAnimator)); 306 } 307 if (Float.compare(mViewBounds.getAlpha(), toTransform.viewOutlineAlpha) != 0) { 308 mOutlineAnimator = ObjectAnimator.ofFloat(this, VIEW_OUTLINE_ALPHA, 309 mViewBounds.getAlpha(), toTransform.viewOutlineAlpha); 310 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, mOutlineAnimator)); 311 } 312 if (updateCallback != null) { 313 ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1); 314 updateCallbackAnim.addUpdateListener(updateCallback); 315 mTmpAnimators.add(toAnimation.apply(AnimationProps.BOUNDS, updateCallbackAnim)); 316 } 317 318 // Create the animator 319 mTransformAnimation = toAnimation.createAnimator(mTmpAnimators); 320 mTransformAnimation.start(); 321 mTargetAnimationTransform.copyFrom(toTransform); 322 } 323 } 324 325 /** Resets this view's properties */ resetViewProperties()326 void resetViewProperties() { 327 cancelTransformAnimation(); 328 setDimAlpha(0); 329 setVisibility(View.VISIBLE); 330 getViewBounds().reset(); 331 getHeaderView().reset(); 332 TaskViewTransform.reset(this); 333 334 mActionButtonView.setScaleX(1f); 335 mActionButtonView.setScaleY(1f); 336 mActionButtonView.setAlpha(0f); 337 mActionButtonView.setTranslationX(0f); 338 mActionButtonView.setTranslationY(0f); 339 mActionButtonView.setTranslationZ(mActionButtonTranslationZ); 340 if (mIncompatibleAppToastView != null) { 341 mIncompatibleAppToastView.setVisibility(View.INVISIBLE); 342 } 343 } 344 345 /** 346 * @return whether we are animating towards {@param transform} 347 */ isAnimatingTo(TaskViewTransform transform)348 boolean isAnimatingTo(TaskViewTransform transform) { 349 return mTransformAnimation != null && mTransformAnimation.isStarted() 350 && mTargetAnimationTransform.isSame(transform); 351 } 352 353 /** 354 * Cancels any current transform animations. 355 */ cancelTransformAnimation()356 public void cancelTransformAnimation() { 357 Utilities.cancelAnimationWithoutCallbacks(mTransformAnimation); 358 Utilities.cancelAnimationWithoutCallbacks(mDimAnimator); 359 Utilities.cancelAnimationWithoutCallbacks(mOutlineAnimator); 360 } 361 362 /** Enables/disables handling touch on this task view. */ setTouchEnabled(boolean enabled)363 void setTouchEnabled(boolean enabled) { 364 setOnClickListener(enabled ? this : null); 365 } 366 367 /** Animates this task view if the user does not interact with the stack after a certain time. */ startNoUserInteractionAnimation()368 void startNoUserInteractionAnimation() { 369 mHeaderView.startNoUserInteractionAnimation(); 370 } 371 372 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ setNoUserInteractionState()373 void setNoUserInteractionState() { 374 mHeaderView.setNoUserInteractionState(); 375 } 376 377 /** Resets the state tracking that the user has not interacted with the stack after a certain time. */ resetNoUserInteractionState()378 void resetNoUserInteractionState() { 379 mHeaderView.resetNoUserInteractionState(); 380 } 381 382 /** Dismisses this task. */ dismissTask()383 void dismissTask() { 384 // Animate out the view and call the callback 385 final TaskView tv = this; 386 DismissTaskViewEvent dismissEvent = new DismissTaskViewEvent(tv); 387 dismissEvent.addPostAnimationCallback(new Runnable() { 388 @Override 389 public void run() { 390 EventBus.getDefault().send(new TaskViewDismissedEvent(mTask, tv, 391 new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION, 392 Interpolators.FAST_OUT_SLOW_IN))); 393 } 394 }); 395 EventBus.getDefault().send(dismissEvent); 396 } 397 398 /** 399 * Returns whether this view should be clipped, or any views below should clip against this 400 * view. 401 */ shouldClipViewInStack()402 boolean shouldClipViewInStack() { 403 // Never clip for freeform tasks or if invisible 404 if (mTask.isFreeformTask() || getVisibility() != View.VISIBLE) { 405 return false; 406 } 407 return mClipViewInStack; 408 } 409 410 /** Sets whether this view should be clipped, or clipped against. */ setClipViewInStack(boolean clip)411 void setClipViewInStack(boolean clip) { 412 if (clip != mClipViewInStack) { 413 mClipViewInStack = clip; 414 if (mCb != null) { 415 mCb.onTaskViewClipStateChanged(this); 416 } 417 } 418 } 419 getHeaderView()420 public TaskViewHeader getHeaderView() { 421 return mHeaderView; 422 } 423 424 /** 425 * Sets the current dim. 426 */ setDimAlpha(float dimAlpha)427 public void setDimAlpha(float dimAlpha) { 428 mDimAlpha = dimAlpha; 429 mThumbnailView.setDimAlpha(dimAlpha); 430 mHeaderView.setDimAlpha(dimAlpha); 431 } 432 433 /** 434 * Sets the current dim without updating the header's dim. 435 */ setDimAlphaWithoutHeader(float dimAlpha)436 public void setDimAlphaWithoutHeader(float dimAlpha) { 437 mDimAlpha = dimAlpha; 438 mThumbnailView.setDimAlpha(dimAlpha); 439 } 440 441 /** 442 * Returns the current dim. 443 */ getDimAlpha()444 public float getDimAlpha() { 445 return mDimAlpha; 446 } 447 448 /** 449 * Explicitly sets the focused state of this task. 450 */ setFocusedState(boolean isFocused, boolean requestViewFocus)451 public void setFocusedState(boolean isFocused, boolean requestViewFocus) { 452 if (isFocused) { 453 if (requestViewFocus && !isFocused()) { 454 requestFocus(); 455 } 456 } else { 457 if (isAccessibilityFocused() && mTouchExplorationEnabled) { 458 clearAccessibilityFocus(); 459 } 460 } 461 } 462 463 /** 464 * Shows the action button. 465 * @param fadeIn whether or not to animate the action button in. 466 * @param fadeInDuration the duration of the action button animation, only used if 467 * {@param fadeIn} is true. 468 */ showActionButton(boolean fadeIn, int fadeInDuration)469 public void showActionButton(boolean fadeIn, int fadeInDuration) { 470 mActionButtonView.setVisibility(View.VISIBLE); 471 472 if (fadeIn && mActionButtonView.getAlpha() < 1f) { 473 mActionButtonView.animate() 474 .alpha(1f) 475 .scaleX(1f) 476 .scaleY(1f) 477 .setDuration(fadeInDuration) 478 .setInterpolator(Interpolators.ALPHA_IN) 479 .start(); 480 } else { 481 mActionButtonView.setScaleX(1f); 482 mActionButtonView.setScaleY(1f); 483 mActionButtonView.setAlpha(1f); 484 mActionButtonView.setTranslationZ(mActionButtonTranslationZ); 485 } 486 } 487 488 /** 489 * Immediately hides the action button. 490 * 491 * @param fadeOut whether or not to animate the action button out. 492 */ hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, final Animator.AnimatorListener animListener)493 public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown, 494 final Animator.AnimatorListener animListener) { 495 if (fadeOut && mActionButtonView.getAlpha() > 0f) { 496 if (scaleDown) { 497 float toScale = 0.9f; 498 mActionButtonView.animate() 499 .scaleX(toScale) 500 .scaleY(toScale); 501 } 502 mActionButtonView.animate() 503 .alpha(0f) 504 .setDuration(fadeOutDuration) 505 .setInterpolator(Interpolators.ALPHA_OUT) 506 .withEndAction(new Runnable() { 507 @Override 508 public void run() { 509 if (animListener != null) { 510 animListener.onAnimationEnd(null); 511 } 512 mActionButtonView.setVisibility(View.INVISIBLE); 513 } 514 }) 515 .start(); 516 } else { 517 mActionButtonView.setAlpha(0f); 518 mActionButtonView.setVisibility(View.INVISIBLE); 519 if (animListener != null) { 520 animListener.onAnimationEnd(null); 521 } 522 } 523 } 524 525 /**** TaskStackAnimationHelper.Callbacks Implementation ****/ 526 527 @Override onPrepareLaunchTargetForEnterAnimation()528 public void onPrepareLaunchTargetForEnterAnimation() { 529 // These values will be animated in when onStartLaunchTargetEnterAnimation() is called 530 setDimAlphaWithoutHeader(0); 531 mActionButtonView.setAlpha(0f); 532 if (mIncompatibleAppToastView != null && 533 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) { 534 mIncompatibleAppToastView.setAlpha(0f); 535 } 536 } 537 538 @Override onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger)539 public void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, 540 boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger) { 541 Utilities.cancelAnimationWithoutCallbacks(mDimAnimator); 542 543 // Dim the view after the app window transitions down into recents 544 postAnimationTrigger.increment(); 545 AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT); 546 mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this, 547 DIM_ALPHA_WITHOUT_HEADER, getDimAlpha(), transform.dimAlpha)); 548 mDimAnimator.addListener(postAnimationTrigger.decrementOnAnimationEnd()); 549 mDimAnimator.start(); 550 551 if (screenPinningEnabled) { 552 showActionButton(true /* fadeIn */, duration /* fadeInDuration */); 553 } 554 555 if (mIncompatibleAppToastView != null && 556 mIncompatibleAppToastView.getVisibility() == View.VISIBLE) { 557 mIncompatibleAppToastView.animate() 558 .alpha(1f) 559 .setDuration(duration) 560 .setInterpolator(Interpolators.ALPHA_IN) 561 .start(); 562 } 563 } 564 565 @Override onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, ReferenceCountedTrigger postAnimationTrigger)566 public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, 567 ReferenceCountedTrigger postAnimationTrigger) { 568 Utilities.cancelAnimationWithoutCallbacks(mDimAnimator); 569 570 // Un-dim the view before/while launching the target 571 AnimationProps animation = new AnimationProps(duration, Interpolators.ALPHA_OUT); 572 mDimAnimator = animation.apply(AnimationProps.DIM_ALPHA, ObjectAnimator.ofFloat(this, 573 DIM_ALPHA, getDimAlpha(), 0)); 574 mDimAnimator.start(); 575 576 postAnimationTrigger.increment(); 577 hideActionButton(true /* fadeOut */, duration, 578 !screenPinningRequested /* scaleDown */, 579 postAnimationTrigger.decrementOnAnimationEnd()); 580 } 581 582 @Override onStartFrontTaskEnterAnimation(boolean screenPinningEnabled)583 public void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled) { 584 if (screenPinningEnabled) { 585 showActionButton(false /* fadeIn */, 0 /* fadeInDuration */); 586 } 587 } 588 589 /**** TaskCallbacks Implementation ****/ 590 onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation, Rect displayRect)591 public void onTaskBound(Task t, boolean touchExplorationEnabled, int displayOrientation, 592 Rect displayRect) { 593 SystemServicesProxy ssp = Recents.getSystemServices(); 594 mTouchExplorationEnabled = touchExplorationEnabled; 595 mTask = t; 596 mTask.addCallback(this); 597 mIsDisabledInSafeMode = !mTask.isSystemApp && ssp.isInSafeMode(); 598 mThumbnailView.bindToTask(mTask, mIsDisabledInSafeMode, displayOrientation, displayRect); 599 mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode); 600 601 if (!t.isDockable && ssp.hasDockedTask()) { 602 if (mIncompatibleAppToastView == null) { 603 mIncompatibleAppToastView = Utilities.findViewStubById(this, 604 R.id.incompatible_app_toast_stub).inflate(); 605 TextView msg = (TextView) findViewById(com.android.internal.R.id.message); 606 msg.setText(R.string.recents_incompatible_app_message); 607 } 608 mIncompatibleAppToastView.setVisibility(View.VISIBLE); 609 } else if (mIncompatibleAppToastView != null) { 610 mIncompatibleAppToastView.setVisibility(View.INVISIBLE); 611 } 612 } 613 614 @Override onTaskDataLoaded(Task task, ActivityManager.TaskThumbnailInfo thumbnailInfo)615 public void onTaskDataLoaded(Task task, ActivityManager.TaskThumbnailInfo thumbnailInfo) { 616 // Update each of the views to the new task data 617 mThumbnailView.onTaskDataLoaded(thumbnailInfo); 618 mHeaderView.onTaskDataLoaded(); 619 } 620 621 @Override onTaskDataUnloaded()622 public void onTaskDataUnloaded() { 623 // Unbind each of the views from the task and remove the task callback 624 mTask.removeCallback(this); 625 mThumbnailView.unbindFromTask(); 626 mHeaderView.unbindFromTask(mTouchExplorationEnabled); 627 } 628 629 @Override onTaskStackIdChanged()630 public void onTaskStackIdChanged() { 631 // Force rebind the header, the thumbnail does not change due to stack changes 632 mHeaderView.bindToTask(mTask, mTouchExplorationEnabled, mIsDisabledInSafeMode); 633 mHeaderView.onTaskDataLoaded(); 634 } 635 636 /**** View.OnClickListener Implementation ****/ 637 638 @Override onClick(final View v)639 public void onClick(final View v) { 640 if (mIsDisabledInSafeMode) { 641 Context context = getContext(); 642 String msg = context.getString(R.string.recents_launch_disabled_message, mTask.title); 643 if (mDisabledAppToast != null) { 644 mDisabledAppToast.cancel(); 645 } 646 mDisabledAppToast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); 647 mDisabledAppToast.show(); 648 return; 649 } 650 651 boolean screenPinningRequested = false; 652 if (v == mActionButtonView) { 653 // Reset the translation of the action button before we animate it out 654 mActionButtonView.setTranslationZ(0f); 655 screenPinningRequested = true; 656 } 657 EventBus.getDefault().send(new LaunchTaskEvent(this, mTask, null, INVALID_STACK_ID, 658 screenPinningRequested)); 659 660 MetricsLogger.action(v.getContext(), MetricsEvent.ACTION_OVERVIEW_SELECT, 661 mTask.key.getComponent().toString()); 662 } 663 664 /**** View.OnLongClickListener Implementation ****/ 665 666 @Override onLongClick(View v)667 public boolean onLongClick(View v) { 668 SystemServicesProxy ssp = Recents.getSystemServices(); 669 // Since we are clipping the view to the bounds, manually do the hit test 670 Rect clipBounds = new Rect(mViewBounds.mClipBounds); 671 clipBounds.scale(getScaleX()); 672 boolean inBounds = clipBounds.contains(mDownTouchPos.x, mDownTouchPos.y); 673 if (v == this && inBounds && !ssp.hasDockedTask()) { 674 // Start listening for drag events 675 setClipViewInStack(false); 676 677 mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2; 678 mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2; 679 680 EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY + 1); 681 EventBus.getDefault().send(new DragStartEvent(mTask, this, mDownTouchPos)); 682 return true; 683 } 684 return false; 685 } 686 687 /**** Events ****/ 688 onBusEvent(DragEndEvent event)689 public final void onBusEvent(DragEndEvent event) { 690 if (!(event.dropTarget instanceof TaskStack.DockState)) { 691 event.addPostAnimationCallback(() -> { 692 // Reset the clip state for the drag view after the end animation completes 693 setClipViewInStack(true); 694 }); 695 } 696 EventBus.getDefault().unregister(this); 697 } 698 onBusEvent(DragEndCancelledEvent event)699 public final void onBusEvent(DragEndCancelledEvent event) { 700 // Reset the clip state for the drag view after the cancel animation completes 701 event.addPostAnimationCallback(() -> { 702 setClipViewInStack(true); 703 }); 704 } 705 } 706