1 /* 2 * Copyright (C) 2020 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.wm.shell.bubbles; 18 19 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 22 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 23 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 24 25 import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; 26 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 27 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 28 import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT; 29 30 import android.annotation.NonNull; 31 import android.annotation.SuppressLint; 32 import android.app.ActivityOptions; 33 import android.app.ActivityTaskManager; 34 import android.app.PendingIntent; 35 import android.content.ComponentName; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.res.Resources; 39 import android.content.res.TypedArray; 40 import android.graphics.Bitmap; 41 import android.graphics.Color; 42 import android.graphics.CornerPathEffect; 43 import android.graphics.Outline; 44 import android.graphics.Paint; 45 import android.graphics.Picture; 46 import android.graphics.PointF; 47 import android.graphics.Rect; 48 import android.graphics.drawable.ShapeDrawable; 49 import android.os.RemoteException; 50 import android.util.AttributeSet; 51 import android.util.FloatProperty; 52 import android.util.IntProperty; 53 import android.util.Log; 54 import android.util.TypedValue; 55 import android.view.LayoutInflater; 56 import android.view.SurfaceControl; 57 import android.view.View; 58 import android.view.ViewGroup; 59 import android.view.ViewOutlineProvider; 60 import android.view.accessibility.AccessibilityNodeInfo; 61 import android.widget.FrameLayout; 62 import android.widget.LinearLayout; 63 64 import androidx.annotation.Nullable; 65 66 import com.android.internal.annotations.VisibleForTesting; 67 import com.android.internal.policy.ScreenDecorationsUtils; 68 import com.android.wm.shell.R; 69 import com.android.wm.shell.TaskView; 70 import com.android.wm.shell.common.AlphaOptimizedButton; 71 import com.android.wm.shell.common.TriangleShape; 72 73 import java.io.PrintWriter; 74 75 /** 76 * Container for the expanded bubble view, handles rendering the caret and settings icon. 77 */ 78 public class BubbleExpandedView extends LinearLayout { 79 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; 80 81 /** {@link IntProperty} for updating bottom clip */ 82 public static final IntProperty<BubbleExpandedView> BOTTOM_CLIP_PROPERTY = 83 new IntProperty<BubbleExpandedView>("bottomClip") { 84 @Override 85 public void setValue(BubbleExpandedView expandedView, int value) { 86 expandedView.setBottomClip(value); 87 } 88 89 @Override 90 public Integer get(BubbleExpandedView expandedView) { 91 return expandedView.mBottomClip; 92 } 93 }; 94 95 /** {@link FloatProperty} for updating taskView or overflow alpha */ 96 public static final FloatProperty<BubbleExpandedView> CONTENT_ALPHA = 97 new FloatProperty<BubbleExpandedView>("contentAlpha") { 98 @Override 99 public void setValue(BubbleExpandedView expandedView, float value) { 100 expandedView.setContentAlpha(value); 101 } 102 103 @Override 104 public Float get(BubbleExpandedView expandedView) { 105 return expandedView.getContentAlpha(); 106 } 107 }; 108 109 /** {@link FloatProperty} for updating background and pointer alpha */ 110 public static final FloatProperty<BubbleExpandedView> BACKGROUND_ALPHA = 111 new FloatProperty<BubbleExpandedView>("backgroundAlpha") { 112 @Override 113 public void setValue(BubbleExpandedView expandedView, float value) { 114 expandedView.setBackgroundAlpha(value); 115 } 116 117 @Override 118 public Float get(BubbleExpandedView expandedView) { 119 return expandedView.getAlpha(); 120 } 121 }; 122 123 /** {@link FloatProperty} for updating manage button alpha */ 124 public static final FloatProperty<BubbleExpandedView> MANAGE_BUTTON_ALPHA = 125 new FloatProperty<BubbleExpandedView>("manageButtonAlpha") { 126 @Override 127 public void setValue(BubbleExpandedView expandedView, float value) { 128 expandedView.mManageButton.setAlpha(value); 129 } 130 131 @Override 132 public Float get(BubbleExpandedView expandedView) { 133 return expandedView.mManageButton.getAlpha(); 134 } 135 }; 136 137 // The triangle pointing to the expanded view 138 private View mPointerView; 139 @Nullable private int[] mExpandedViewContainerLocation; 140 141 private AlphaOptimizedButton mManageButton; 142 private TaskView mTaskView; 143 private BubbleOverflowContainerView mOverflowView; 144 145 private int mTaskId = INVALID_TASK_ID; 146 147 private boolean mImeVisible; 148 private boolean mNeedsNewHeight; 149 150 /** 151 * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If 152 * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha 153 * value until the animation ends. 154 */ 155 private boolean mIsContentVisible = false; 156 157 /** 158 * Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on 159 * applying alpha changes from {@link #setContentVisibility} until the animation ends. 160 */ 161 private boolean mIsAnimating = false; 162 163 private int mPointerWidth; 164 private int mPointerHeight; 165 private float mPointerRadius; 166 private float mPointerOverlap; 167 private final PointF mPointerPos = new PointF(); 168 private CornerPathEffect mPointerEffect; 169 private ShapeDrawable mCurrentPointer; 170 private ShapeDrawable mTopPointer; 171 private ShapeDrawable mLeftPointer; 172 private ShapeDrawable mRightPointer; 173 private float mCornerRadius = 0f; 174 private int mBackgroundColorFloating; 175 private boolean mUsingMaxHeight; 176 private int mTopClip = 0; 177 private int mBottomClip = 0; 178 @Nullable private Bubble mBubble; 179 private PendingIntent mPendingIntent; 180 // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead 181 private boolean mIsOverflow; 182 private boolean mIsClipping; 183 184 private BubbleController mController; 185 private BubbleStackView mStackView; 186 private BubblePositioner mPositioner; 187 188 /** 189 * Container for the {@code TaskView} that has a solid, round-rect background that shows if the 190 * {@code TaskView} hasn't loaded. 191 */ 192 private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext()); 193 194 private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { 195 private boolean mInitialized = false; 196 private boolean mDestroyed = false; 197 198 @Override 199 public void onInitialized() { 200 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 201 Log.d(TAG, "onInitialized: destroyed=" + mDestroyed 202 + " initialized=" + mInitialized 203 + " bubble=" + getBubbleKey()); 204 } 205 206 if (mDestroyed || mInitialized) { 207 return; 208 } 209 210 // Custom options so there is no activity transition animation 211 ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), 212 0 /* enterResId */, 0 /* exitResId */); 213 214 Rect launchBounds = new Rect(); 215 mTaskView.getBoundsOnScreen(launchBounds); 216 217 // TODO: I notice inconsistencies in lifecycle 218 // Post to keep the lifecycle normal 219 post(() -> { 220 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 221 Log.d(TAG, "onInitialized: calling startActivity, bubble=" 222 + getBubbleKey()); 223 } 224 try { 225 options.setTaskAlwaysOnTop(true); 226 options.setLaunchedFromBubble(true); 227 228 Intent fillInIntent = new Intent(); 229 // Apply flags to make behaviour match documentLaunchMode=always. 230 fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); 231 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); 232 233 if (mBubble.isAppBubble()) { 234 PendingIntent pi = PendingIntent.getActivity(mContext, 0, 235 mBubble.getAppBubbleIntent(), 236 PendingIntent.FLAG_MUTABLE, 237 null); 238 mTaskView.startActivity(pi, fillInIntent, options, launchBounds); 239 } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { 240 options.setApplyActivityFlagsForBubbles(true); 241 mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), 242 options, launchBounds); 243 } else { 244 if (mBubble != null) { 245 mBubble.setIntentActive(); 246 } 247 mTaskView.startActivity(mPendingIntent, fillInIntent, options, 248 launchBounds); 249 } 250 } catch (RuntimeException e) { 251 // If there's a runtime exception here then there's something 252 // wrong with the intent, we can't really recover / try to populate 253 // the bubble again so we'll just remove it. 254 Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() 255 + ", " + e.getMessage() + "; removing bubble"); 256 mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); 257 } 258 }); 259 mInitialized = true; 260 } 261 262 @Override 263 public void onReleased() { 264 mDestroyed = true; 265 } 266 267 @Override 268 public void onTaskCreated(int taskId, ComponentName name) { 269 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 270 Log.d(TAG, "onTaskCreated: taskId=" + taskId 271 + " bubble=" + getBubbleKey()); 272 } 273 // The taskId is saved to use for removeTask, preventing appearance in recent tasks. 274 mTaskId = taskId; 275 276 // With the task org, the taskAppeared callback will only happen once the task has 277 // already drawn 278 setContentVisibility(true); 279 } 280 281 @Override 282 public void onTaskVisibilityChanged(int taskId, boolean visible) { 283 setContentVisibility(visible); 284 } 285 286 @Override 287 public void onTaskRemovalStarted(int taskId) { 288 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 289 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId 290 + " bubble=" + getBubbleKey()); 291 } 292 if (mBubble != null) { 293 // Must post because this is called from a binder thread. 294 post(() -> mController.removeBubble( 295 mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED)); 296 } 297 } 298 299 @Override 300 public void onBackPressedOnTaskRoot(int taskId) { 301 if (mTaskId == taskId && mStackView.isExpanded()) { 302 mStackView.onBackPressed(); 303 } 304 } 305 }; 306 BubbleExpandedView(Context context)307 public BubbleExpandedView(Context context) { 308 this(context, null); 309 } 310 BubbleExpandedView(Context context, AttributeSet attrs)311 public BubbleExpandedView(Context context, AttributeSet attrs) { 312 this(context, attrs, 0); 313 } 314 BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)315 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { 316 this(context, attrs, defStyleAttr, 0); 317 } 318 BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)319 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, 320 int defStyleRes) { 321 super(context, attrs, defStyleAttr, defStyleRes); 322 } 323 324 @SuppressLint("ClickableViewAccessibility") 325 @Override onFinishInflate()326 protected void onFinishInflate() { 327 super.onFinishInflate(); 328 mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate( 329 R.layout.bubble_manage_button, this /* parent */, false /* attach */); 330 updateDimensions(); 331 mPointerView = findViewById(R.id.pointer_view); 332 mCurrentPointer = mTopPointer; 333 mPointerView.setVisibility(INVISIBLE); 334 335 // Set {@code TaskView}'s alpha value as zero, since there is no view content to be shown. 336 setContentVisibility(false); 337 338 mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() { 339 @Override 340 public void getOutline(View view, Outline outline) { 341 Rect clip = new Rect(0, mTopClip, view.getWidth(), view.getHeight() - mBottomClip); 342 outline.setRoundRect(clip, mCornerRadius); 343 } 344 }); 345 mExpandedViewContainer.setClipToOutline(true); 346 mExpandedViewContainer.setLayoutParams( 347 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 348 addView(mExpandedViewContainer); 349 350 // Expanded stack layout, top to bottom: 351 // Expanded view container 352 // ==> bubble row 353 // ==> expanded view 354 // ==> activity view 355 // ==> manage button 356 bringChildToFront(mManageButton); 357 358 applyThemeAttrs(); 359 360 setClipToPadding(false); 361 setOnTouchListener((view, motionEvent) -> { 362 if (mTaskView == null) { 363 return false; 364 } 365 366 final Rect avBounds = new Rect(); 367 mTaskView.getBoundsOnScreen(avBounds); 368 369 // Consume and ignore events on the expanded view padding that are within the 370 // {@code TaskView}'s vertical bounds. These events are part of a back gesture, and so 371 // they should not collapse the stack (which all other touches on areas around the AV 372 // would do). 373 if (motionEvent.getRawY() >= avBounds.top 374 && motionEvent.getRawY() <= avBounds.bottom 375 && (motionEvent.getRawX() < avBounds.left 376 || motionEvent.getRawX() > avBounds.right)) { 377 return true; 378 } 379 380 return false; 381 }); 382 383 // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout 384 // so the Manage button appears on the right. 385 setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 386 } 387 388 /** 389 * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need 390 * to be called after view inflate. 391 */ initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow)392 void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) { 393 mController = controller; 394 mStackView = stackView; 395 mIsOverflow = isOverflow; 396 mPositioner = mController.getPositioner(); 397 398 if (mIsOverflow) { 399 mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( 400 R.layout.bubble_overflow_container, null /* root */); 401 mOverflowView.setBubbleController(mController); 402 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); 403 mExpandedViewContainer.addView(mOverflowView, lp); 404 mExpandedViewContainer.setLayoutParams( 405 new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 406 bringChildToFront(mOverflowView); 407 mManageButton.setVisibility(GONE); 408 } else { 409 mTaskView = new TaskView(mContext, mController.getTaskOrganizer(), 410 mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); 411 mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); 412 mExpandedViewContainer.addView(mTaskView); 413 bringChildToFront(mTaskView); 414 } 415 } 416 updateDimensions()417 void updateDimensions() { 418 Resources res = getResources(); 419 updateFontSize(); 420 421 mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); 422 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); 423 mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius); 424 mPointerEffect = new CornerPathEffect(mPointerRadius); 425 mPointerOverlap = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_overlap); 426 mTopPointer = new ShapeDrawable(TriangleShape.create( 427 mPointerWidth, mPointerHeight, true /* pointUp */)); 428 mLeftPointer = new ShapeDrawable(TriangleShape.createHorizontal( 429 mPointerWidth, mPointerHeight, true /* pointLeft */)); 430 mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal( 431 mPointerWidth, mPointerHeight, false /* pointLeft */)); 432 if (mPointerView != null) { 433 updatePointerView(); 434 } 435 436 if (mManageButton != null) { 437 int visibility = mManageButton.getVisibility(); 438 removeView(mManageButton); 439 mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate( 440 R.layout.bubble_manage_button, this /* parent */, false /* attach */); 441 addView(mManageButton); 442 mManageButton.setVisibility(visibility); 443 } 444 } 445 updateFontSize()446 void updateFontSize() { 447 final float fontSize = mContext.getResources() 448 .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material); 449 if (mManageButton != null) { 450 mManageButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); 451 } 452 if (mOverflowView != null) { 453 mOverflowView.updateFontSize(); 454 } 455 } 456 applyThemeAttrs()457 void applyThemeAttrs() { 458 final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ 459 android.R.attr.dialogCornerRadius, 460 android.R.attr.colorBackgroundFloating}); 461 boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( 462 mContext.getResources()); 463 mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; 464 mBackgroundColorFloating = ta.getColor(1, Color.WHITE); 465 mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating); 466 ta.recycle(); 467 468 if (mTaskView != null) { 469 mTaskView.setCornerRadius(mCornerRadius); 470 } 471 updatePointerView(); 472 } 473 474 /** Updates the size and visuals of the pointer. **/ updatePointerView()475 private void updatePointerView() { 476 LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); 477 if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { 478 lp.width = mPointerHeight; 479 lp.height = mPointerWidth; 480 } else { 481 lp.width = mPointerWidth; 482 lp.height = mPointerHeight; 483 } 484 mCurrentPointer.setTint(mBackgroundColorFloating); 485 486 Paint arrowPaint = mCurrentPointer.getPaint(); 487 arrowPaint.setColor(mBackgroundColorFloating); 488 arrowPaint.setPathEffect(mPointerEffect); 489 mPointerView.setLayoutParams(lp); 490 mPointerView.setBackground(mCurrentPointer); 491 } 492 493 @VisibleForTesting getBubbleKey()494 public String getBubbleKey() { 495 return mBubble != null ? mBubble.getKey() : mIsOverflow ? BubbleOverflow.KEY : null; 496 } 497 498 /** 499 * Sets whether the surface displaying app content should sit on top. This is useful for 500 * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble 501 * being dragged out, the manage menu) this is set to false, otherwise it should be true. 502 */ setSurfaceZOrderedOnTop(boolean onTop)503 public void setSurfaceZOrderedOnTop(boolean onTop) { 504 if (mTaskView == null) { 505 return; 506 } 507 mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); 508 } 509 setImeVisible(boolean visible)510 void setImeVisible(boolean visible) { 511 mImeVisible = visible; 512 if (!mImeVisible && mNeedsNewHeight) { 513 updateHeight(); 514 } 515 } 516 517 /** Return a GraphicBuffer with the contents of the task view surface. */ 518 @Nullable snapshotActivitySurface()519 SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() { 520 if (mIsOverflow) { 521 // For now, just snapshot the view and return it as a hw buffer so that the animation 522 // code for both the tasks and overflow can be the same 523 Picture p = new Picture(); 524 mOverflowView.draw( 525 p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight())); 526 p.endRecording(); 527 Bitmap snapshot = Bitmap.createBitmap(p); 528 return new SurfaceControl.ScreenshotHardwareBuffer( 529 snapshot.getHardwareBuffer(), 530 snapshot.getColorSpace(), 531 false /* containsSecureLayers */, 532 false /* containsHdrLayers */); 533 } 534 if (mTaskView == null || mTaskView.getSurfaceControl() == null) { 535 return null; 536 } 537 return SurfaceControl.captureLayers( 538 mTaskView.getSurfaceControl(), 539 new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()), 540 1 /* scale */); 541 } 542 getTaskViewLocationOnScreen()543 int[] getTaskViewLocationOnScreen() { 544 if (mIsOverflow) { 545 // This is only used for animating away the surface when switching bubbles, just use the 546 // view location on screen for now to allow us to use the same animation code with tasks 547 return mOverflowView.getLocationOnScreen(); 548 } 549 if (mTaskView != null) { 550 return mTaskView.getLocationOnScreen(); 551 } else { 552 return new int[]{0, 0}; 553 } 554 } 555 556 // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this setManageClickListener(OnClickListener manageClickListener)557 void setManageClickListener(OnClickListener manageClickListener) { 558 mManageButton.setOnClickListener(manageClickListener); 559 } 560 561 /** 562 * Updates the obscured touchable region for the task surface. This calls onLocationChanged, 563 * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is 564 * useful if a view has been added or removed from on top of the {@code TaskView}, such as the 565 * manage menu. 566 */ updateObscuredTouchableRegion()567 void updateObscuredTouchableRegion() { 568 if (mTaskView != null) { 569 mTaskView.onLocationChanged(); 570 } 571 } 572 573 @Override onDetachedFromWindow()574 protected void onDetachedFromWindow() { 575 super.onDetachedFromWindow(); 576 mImeVisible = false; 577 mNeedsNewHeight = false; 578 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 579 Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); 580 } 581 } 582 583 /** 584 * Whether we are currently animating the {@code TaskView}. If this is set to 585 * true, calls to {@link #setContentVisibility} will not be applied until this is set to false 586 * again. 587 */ setAnimating(boolean animating)588 public void setAnimating(boolean animating) { 589 mIsAnimating = animating; 590 591 // If we're done animating, apply the correct 592 if (!animating) { 593 setContentVisibility(mIsContentVisible); 594 } 595 } 596 597 /** 598 * Get alpha from underlying {@code TaskView} if this view is for a bubble. 599 * Or get alpha for the overflow view if this view is for overflow. 600 * 601 * @return alpha for the content being shown 602 */ getContentAlpha()603 public float getContentAlpha() { 604 if (mIsOverflow) { 605 return mOverflowView.getAlpha(); 606 } 607 if (mTaskView != null) { 608 return mTaskView.getAlpha(); 609 } 610 return 1f; 611 } 612 613 /** 614 * Set alpha of the underlying {@code TaskView} if this view is for a bubble. 615 * Or set alpha for the overflow view if this view is for overflow. 616 * 617 * Changing expanded view's alpha does not affect the {@code TaskView} since it uses a Surface. 618 */ setContentAlpha(float alpha)619 public void setContentAlpha(float alpha) { 620 if (mIsOverflow) { 621 mOverflowView.setAlpha(alpha); 622 } else if (mTaskView != null) { 623 mTaskView.setAlpha(alpha); 624 } 625 } 626 627 /** 628 * Sets the alpha of the background and the pointer view. 629 */ setBackgroundAlpha(float alpha)630 public void setBackgroundAlpha(float alpha) { 631 mPointerView.setAlpha(alpha); 632 setAlpha(alpha); 633 } 634 635 /** 636 * Set translation Y for the expanded view content. 637 * Excludes manage button and pointer. 638 */ setContentTranslationY(float translationY)639 public void setContentTranslationY(float translationY) { 640 mExpandedViewContainer.setTranslationY(translationY); 641 642 // Left or right pointer can become detached when moving the view up 643 if (translationY <= 0 && (isShowingLeftPointer() || isShowingRightPointer())) { 644 // Y coordinate where the pointer would start to get detached from the expanded view. 645 // Takes into account bottom clipping and rounded corners 646 float detachPoint = 647 mExpandedViewContainer.getBottom() - mBottomClip - mCornerRadius + translationY; 648 float pointerBottom = mPointerPos.y + mPointerHeight; 649 // If pointer bottom is past detach point, move it in by that many pixels 650 float horizontalShift = 0; 651 if (pointerBottom > detachPoint) { 652 horizontalShift = pointerBottom - detachPoint; 653 } 654 if (isShowingLeftPointer()) { 655 // Move left pointer right 656 movePointerBy(horizontalShift, 0); 657 } else { 658 // Move right pointer left 659 movePointerBy(-horizontalShift, 0); 660 } 661 // Hide pointer if it is moved by entire width 662 mPointerView.setVisibility( 663 horizontalShift > mPointerWidth ? View.INVISIBLE : View.VISIBLE); 664 } 665 } 666 667 /** 668 * Update alpha value for the manage button 669 */ setManageButtonAlpha(float alpha)670 public void setManageButtonAlpha(float alpha) { 671 mManageButton.setAlpha(alpha); 672 } 673 674 /** 675 * Set {@link #setTranslationY(float) translationY} for the manage button 676 */ setManageButtonTranslationY(float translationY)677 public void setManageButtonTranslationY(float translationY) { 678 mManageButton.setTranslationY(translationY); 679 } 680 681 /** 682 * Set top clipping for the view 683 */ setTopClip(int clip)684 public void setTopClip(int clip) { 685 mTopClip = clip; 686 onContainerClipUpdate(); 687 } 688 689 /** 690 * Set bottom clipping for the view 691 */ setBottomClip(int clip)692 public void setBottomClip(int clip) { 693 mBottomClip = clip; 694 onContainerClipUpdate(); 695 } 696 onContainerClipUpdate()697 private void onContainerClipUpdate() { 698 if (mTopClip == 0 && mBottomClip == 0) { 699 if (mIsClipping) { 700 mIsClipping = false; 701 if (mTaskView != null) { 702 mTaskView.setClipBounds(null); 703 mTaskView.setEnableSurfaceClipping(false); 704 } 705 mExpandedViewContainer.invalidateOutline(); 706 } 707 } else { 708 if (!mIsClipping) { 709 mIsClipping = true; 710 if (mTaskView != null) { 711 mTaskView.setEnableSurfaceClipping(true); 712 } 713 } 714 mExpandedViewContainer.invalidateOutline(); 715 if (mTaskView != null) { 716 mTaskView.setClipBounds(new Rect(0, mTopClip, mTaskView.getWidth(), 717 mTaskView.getHeight() - mBottomClip)); 718 } 719 } 720 } 721 722 /** 723 * Move pointer from base position 724 */ movePointerBy(float x, float y)725 public void movePointerBy(float x, float y) { 726 mPointerView.setTranslationX(mPointerPos.x + x); 727 mPointerView.setTranslationY(mPointerPos.y + y); 728 } 729 730 /** 731 * Set visibility of contents in the expanded state. 732 * 733 * @param visibility {@code true} if the contents should be visible on the screen. 734 * 735 * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 736 * and setting {@code false} actually means rendering the contents in transparent. 737 */ setContentVisibility(boolean visibility)738 public void setContentVisibility(boolean visibility) { 739 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 740 Log.d(TAG, "setContentVisibility: visibility=" + visibility 741 + " bubble=" + getBubbleKey()); 742 } 743 mIsContentVisible = visibility; 744 if (mTaskView != null && !mIsAnimating) { 745 mTaskView.setAlpha(visibility ? 1f : 0f); 746 mPointerView.setAlpha(visibility ? 1f : 0f); 747 } 748 } 749 750 @Nullable getTaskView()751 TaskView getTaskView() { 752 return mTaskView; 753 } 754 755 @VisibleForTesting getOverflow()756 public BubbleOverflowContainerView getOverflow() { 757 return mOverflowView; 758 } 759 760 761 /** 762 * Return content height: taskView or overflow. 763 * Takes into account clippings set by {@link #setTopClip(int)} and {@link #setBottomClip(int)} 764 * 765 * @return if bubble is for overflow, return overflow height, otherwise return taskView height 766 */ getContentHeight()767 public int getContentHeight() { 768 if (mIsOverflow) { 769 return mOverflowView.getHeight() - mTopClip - mBottomClip; 770 } 771 if (mTaskView != null) { 772 return mTaskView.getHeight() - mTopClip - mBottomClip; 773 } 774 return 0; 775 } 776 777 /** 778 * Return bottom position of the content on screen 779 * 780 * @return if bubble is for overflow, return value for overflow, otherwise taskView 781 */ getContentBottomOnScreen()782 public int getContentBottomOnScreen() { 783 Rect out = new Rect(); 784 if (mIsOverflow) { 785 mOverflowView.getBoundsOnScreen(out); 786 } 787 if (mTaskView != null) { 788 mTaskView.getBoundsOnScreen(out); 789 } 790 return out.bottom; 791 } 792 getTaskId()793 int getTaskId() { 794 return mTaskId; 795 } 796 797 /** 798 * Sets the bubble used to populate this view. 799 */ update(Bubble bubble)800 void update(Bubble bubble) { 801 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 802 Log.d(TAG, "update: bubble=" + bubble); 803 } 804 if (mStackView == null) { 805 Log.w(TAG, "Stack is null for bubble: " + bubble); 806 return; 807 } 808 boolean isNew = mBubble == null || didBackingContentChange(bubble); 809 if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { 810 mBubble = bubble; 811 mManageButton.setContentDescription(getResources().getString( 812 R.string.bubbles_settings_button_description, bubble.getAppName())); 813 mManageButton.setAccessibilityDelegate( 814 new AccessibilityDelegate() { 815 @Override 816 public void onInitializeAccessibilityNodeInfo(View host, 817 AccessibilityNodeInfo info) { 818 super.onInitializeAccessibilityNodeInfo(host, info); 819 // On focus, have TalkBack say 820 // "Actions available. Use swipe up then right to view." 821 // in addition to the default "double tap to activate". 822 mStackView.setupLocalMenu(info); 823 } 824 }); 825 826 if (isNew) { 827 mPendingIntent = mBubble.getBubbleIntent(); 828 if ((mPendingIntent != null || mBubble.hasMetadataShortcutId()) 829 && mTaskView != null) { 830 setContentVisibility(false); 831 mTaskView.setVisibility(VISIBLE); 832 } 833 } 834 applyThemeAttrs(); 835 } else { 836 Log.w(TAG, "Trying to update entry with different key, new bubble: " 837 + bubble.getKey() + " old bubble: " + bubble.getKey()); 838 } 839 } 840 841 /** 842 * Bubbles are backed by a pending intent or a shortcut, once the activity is 843 * started we never change it / restart it on notification updates -- unless the bubbles' 844 * backing data switches. 845 * 846 * This indicates if the new bubble is backed by a different data source than what was 847 * previously shown here (e.g. previously a pending intent & now a shortcut). 848 * 849 * @param newBubble the bubble this view is being updated with. 850 * @return true if the backing content has changed. 851 */ didBackingContentChange(Bubble newBubble)852 private boolean didBackingContentChange(Bubble newBubble) { 853 boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; 854 boolean newIsIntentBased = newBubble.getBubbleIntent() != null; 855 return prevWasIntentBased != newIsIntentBased; 856 } 857 858 /** 859 * Whether the bubble is using all available height to display or not. 860 */ isUsingMaxHeight()861 public boolean isUsingMaxHeight() { 862 return mUsingMaxHeight; 863 } 864 updateHeight()865 void updateHeight() { 866 if (mExpandedViewContainerLocation == null) { 867 return; 868 } 869 870 if ((mBubble != null && mTaskView != null) || mIsOverflow) { 871 float desiredHeight = mPositioner.getExpandedViewHeight(mBubble); 872 int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow); 873 float height = desiredHeight == MAX_HEIGHT 874 ? maxHeight 875 : Math.min(desiredHeight, maxHeight); 876 mUsingMaxHeight = height == maxHeight; 877 FrameLayout.LayoutParams lp = mIsOverflow 878 ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams() 879 : (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); 880 mNeedsNewHeight = lp.height != height; 881 if (!mImeVisible) { 882 // If the ime is visible... don't adjust the height because that will cause 883 // a configuration change and the ime will be lost. 884 lp.height = (int) height; 885 if (mIsOverflow) { 886 mOverflowView.setLayoutParams(lp); 887 } else { 888 mTaskView.setLayoutParams(lp); 889 } 890 mNeedsNewHeight = false; 891 } 892 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 893 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() 894 + " height=" + height 895 + " mNeedsNewHeight=" + mNeedsNewHeight); 896 } 897 } 898 } 899 900 /** 901 * Update appearance of the expanded view being displayed. 902 * 903 * @param containerLocationOnScreen The location on-screen of the container the expanded view is 904 * added to. This allows us to calculate max height without 905 * waiting for layout. 906 */ updateView(int[] containerLocationOnScreen)907 public void updateView(int[] containerLocationOnScreen) { 908 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 909 Log.d(TAG, "updateView: bubble=" 910 + getBubbleKey()); 911 } 912 mExpandedViewContainerLocation = containerLocationOnScreen; 913 updateHeight(); 914 if (mTaskView != null 915 && mTaskView.getVisibility() == VISIBLE 916 && mTaskView.isAttachedToWindow()) { 917 mTaskView.onLocationChanged(); 918 } 919 if (mIsOverflow) { 920 post(() -> { 921 mOverflowView.show(); 922 }); 923 } 924 } 925 926 /** 927 * Sets the position of the pointer. 928 * 929 * When bubbles are showing "vertically" they display along the left / right sides of the 930 * screen with the expanded view beside them. 931 * 932 * If they aren't showing vertically they're positioned along the top of the screen with the 933 * expanded view below them. 934 * 935 * @param bubblePosition the x position of the bubble if showing on top, the y position of 936 * the bubble if showing vertically. 937 * @param onLeft whether the stack was on the left side of the screen when expanded. 938 * @param animate whether the pointer should animate to this position. 939 */ setPointerPosition(float bubblePosition, boolean onLeft, boolean animate)940 public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) { 941 final boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection() 942 == LAYOUT_DIRECTION_RTL; 943 // Pointer gets drawn in the padding 944 final boolean showVertically = mPositioner.showBubblesVertically(); 945 final float paddingLeft = (showVertically && onLeft) 946 ? mPointerHeight - mPointerOverlap 947 : 0; 948 final float paddingRight = (showVertically && !onLeft) 949 ? mPointerHeight - mPointerOverlap 950 : 0; 951 final float paddingTop = showVertically 952 ? 0 953 : mPointerHeight - mPointerOverlap; 954 setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0); 955 956 // Subtract the expandedViewY here because the pointer is placed within the expandedView. 957 float pointerPosition = mPositioner.getPointerPosition(bubblePosition); 958 final float bubbleCenter = mPositioner.showBubblesVertically() 959 ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition) 960 : pointerPosition; 961 // Post because we need the width of the view 962 post(() -> { 963 mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; 964 updatePointerView(); 965 if (showVertically) { 966 mPointerPos.y = bubbleCenter - (mPointerWidth / 2f); 967 if (!isRtl) { 968 mPointerPos.x = onLeft 969 ? -mPointerHeight + mPointerOverlap 970 : getWidth() - mPaddingRight - mPointerOverlap; 971 } else { 972 mPointerPos.x = onLeft 973 ? -(getWidth() - mPaddingLeft - mPointerOverlap) 974 : mPointerHeight - mPointerOverlap; 975 } 976 } else { 977 mPointerPos.y = mPointerOverlap; 978 if (!isRtl) { 979 mPointerPos.x = bubbleCenter - (mPointerWidth / 2f); 980 } else { 981 mPointerPos.x = -(getWidth() - mPaddingLeft - bubbleCenter) 982 + (mPointerWidth / 2f); 983 } 984 } 985 if (animate) { 986 mPointerView.animate().translationX(mPointerPos.x).translationY( 987 mPointerPos.y).start(); 988 } else { 989 mPointerView.setTranslationY(mPointerPos.y); 990 mPointerView.setTranslationX(mPointerPos.x); 991 mPointerView.setVisibility(VISIBLE); 992 } 993 }); 994 } 995 996 /** 997 * Return true if pointer is shown on the left 998 */ isShowingLeftPointer()999 public boolean isShowingLeftPointer() { 1000 return mCurrentPointer == mLeftPointer; 1001 } 1002 1003 /** 1004 * Return true if pointer is shown on the right 1005 */ isShowingRightPointer()1006 public boolean isShowingRightPointer() { 1007 return mCurrentPointer == mRightPointer; 1008 } 1009 1010 /** 1011 * Return width of the current pointer 1012 */ getPointerWidth()1013 public int getPointerWidth() { 1014 return mPointerWidth; 1015 } 1016 1017 /** 1018 * Position of the manage button displayed in the expanded view. Used for placing user 1019 * education about the manage button. 1020 */ getManageButtonBoundsOnScreen(Rect rect)1021 public void getManageButtonBoundsOnScreen(Rect rect) { 1022 mManageButton.getBoundsOnScreen(rect); 1023 } 1024 getManageButtonMargin()1025 public int getManageButtonMargin() { 1026 return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart(); 1027 } 1028 1029 /** 1030 * Cleans up anything related to the task and {@code TaskView}. If this view should be reused 1031 * after this method is called, then 1032 * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first. 1033 */ cleanUpExpandedState()1034 public void cleanUpExpandedState() { 1035 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 1036 Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); 1037 } 1038 if (getTaskId() != INVALID_TASK_ID) { 1039 try { 1040 ActivityTaskManager.getService().removeTask(getTaskId()); 1041 } catch (RemoteException e) { 1042 Log.w(TAG, e.getMessage()); 1043 } 1044 } 1045 if (mTaskView != null) { 1046 mTaskView.release(); 1047 removeView(mTaskView); 1048 mTaskView = null; 1049 } 1050 } 1051 1052 /** 1053 * Description of current expanded view state. 1054 */ dump(@onNull PrintWriter pw)1055 public void dump(@NonNull PrintWriter pw) { 1056 pw.print("BubbleExpandedView"); 1057 pw.print(" taskId: "); pw.println(mTaskId); 1058 pw.print(" stackView: "); pw.println(mStackView); 1059 } 1060 } 1061