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.Rect; 47 import android.graphics.drawable.ShapeDrawable; 48 import android.os.RemoteException; 49 import android.util.AttributeSet; 50 import android.util.Log; 51 import android.util.TypedValue; 52 import android.view.LayoutInflater; 53 import android.view.SurfaceControl; 54 import android.view.View; 55 import android.view.ViewGroup; 56 import android.view.ViewOutlineProvider; 57 import android.view.accessibility.AccessibilityNodeInfo; 58 import android.widget.FrameLayout; 59 import android.widget.LinearLayout; 60 61 import androidx.annotation.Nullable; 62 63 import com.android.internal.policy.ScreenDecorationsUtils; 64 import com.android.wm.shell.R; 65 import com.android.wm.shell.TaskView; 66 import com.android.wm.shell.common.AlphaOptimizedButton; 67 import com.android.wm.shell.common.TriangleShape; 68 69 import java.io.FileDescriptor; 70 import java.io.PrintWriter; 71 72 /** 73 * Container for the expanded bubble view, handles rendering the caret and settings icon. 74 */ 75 public class BubbleExpandedView extends LinearLayout { 76 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; 77 78 // The triangle pointing to the expanded view 79 private View mPointerView; 80 @Nullable private int[] mExpandedViewContainerLocation; 81 82 private AlphaOptimizedButton mManageButton; 83 private TaskView mTaskView; 84 private BubbleOverflowContainerView mOverflowView; 85 86 private int mTaskId = INVALID_TASK_ID; 87 88 private boolean mImeVisible; 89 private boolean mNeedsNewHeight; 90 91 /** 92 * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If 93 * {@link #mIsAlphaAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha 94 * value until the animation ends. 95 */ 96 private boolean mIsContentVisible = false; 97 98 /** 99 * Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on 100 * applying alpha changes from {@link #setContentVisibility} until the animation ends. 101 */ 102 private boolean mIsAlphaAnimating = false; 103 104 private int mPointerWidth; 105 private int mPointerHeight; 106 private float mPointerRadius; 107 private float mPointerOverlap; 108 private CornerPathEffect mPointerEffect; 109 private ShapeDrawable mCurrentPointer; 110 private ShapeDrawable mTopPointer; 111 private ShapeDrawable mLeftPointer; 112 private ShapeDrawable mRightPointer; 113 private float mCornerRadius = 0f; 114 private int mBackgroundColorFloating; 115 116 @Nullable private Bubble mBubble; 117 private PendingIntent mPendingIntent; 118 // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead 119 private boolean mIsOverflow; 120 121 private BubbleController mController; 122 private BubbleStackView mStackView; 123 private BubblePositioner mPositioner; 124 125 /** 126 * Container for the {@code TaskView} that has a solid, round-rect background that shows if the 127 * {@code TaskView} hasn't loaded. 128 */ 129 private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext()); 130 131 private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { 132 private boolean mInitialized = false; 133 private boolean mDestroyed = false; 134 135 @Override 136 public void onInitialized() { 137 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 138 Log.d(TAG, "onInitialized: destroyed=" + mDestroyed 139 + " initialized=" + mInitialized 140 + " bubble=" + getBubbleKey()); 141 } 142 143 if (mDestroyed || mInitialized) { 144 return; 145 } 146 147 // Custom options so there is no activity transition animation 148 ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), 149 0 /* enterResId */, 0 /* exitResId */); 150 151 Rect launchBounds = new Rect(); 152 mTaskView.getBoundsOnScreen(launchBounds); 153 154 // TODO: I notice inconsistencies in lifecycle 155 // Post to keep the lifecycle normal 156 post(() -> { 157 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 158 Log.d(TAG, "onInitialized: calling startActivity, bubble=" 159 + getBubbleKey()); 160 } 161 try { 162 options.setTaskAlwaysOnTop(true); 163 options.setLaunchedFromBubble(true); 164 if (!mIsOverflow && mBubble.hasMetadataShortcutId()) { 165 options.setApplyActivityFlagsForBubbles(true); 166 mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), 167 options, launchBounds); 168 } else { 169 Intent fillInIntent = new Intent(); 170 // Apply flags to make behaviour match documentLaunchMode=always. 171 fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); 172 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); 173 if (mBubble != null) { 174 mBubble.setIntentActive(); 175 } 176 mTaskView.startActivity(mPendingIntent, fillInIntent, options, 177 launchBounds); 178 } 179 } catch (RuntimeException e) { 180 // If there's a runtime exception here then there's something 181 // wrong with the intent, we can't really recover / try to populate 182 // the bubble again so we'll just remove it. 183 Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() 184 + ", " + e.getMessage() + "; removing bubble"); 185 mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); 186 } 187 }); 188 mInitialized = true; 189 } 190 191 @Override 192 public void onReleased() { 193 mDestroyed = true; 194 } 195 196 @Override 197 public void onTaskCreated(int taskId, ComponentName name) { 198 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 199 Log.d(TAG, "onTaskCreated: taskId=" + taskId 200 + " bubble=" + getBubbleKey()); 201 } 202 // The taskId is saved to use for removeTask, preventing appearance in recent tasks. 203 mTaskId = taskId; 204 205 // With the task org, the taskAppeared callback will only happen once the task has 206 // already drawn 207 setContentVisibility(true); 208 } 209 210 @Override 211 public void onTaskVisibilityChanged(int taskId, boolean visible) { 212 setContentVisibility(visible); 213 } 214 215 @Override 216 public void onTaskRemovalStarted(int taskId) { 217 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 218 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId 219 + " bubble=" + getBubbleKey()); 220 } 221 if (mBubble != null) { 222 // Must post because this is called from a binder thread. 223 post(() -> mController.removeBubble( 224 mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED)); 225 } 226 } 227 228 @Override 229 public void onBackPressedOnTaskRoot(int taskId) { 230 if (mTaskId == taskId && mStackView.isExpanded()) { 231 mStackView.onBackPressed(); 232 } 233 } 234 }; 235 BubbleExpandedView(Context context)236 public BubbleExpandedView(Context context) { 237 this(context, null); 238 } 239 BubbleExpandedView(Context context, AttributeSet attrs)240 public BubbleExpandedView(Context context, AttributeSet attrs) { 241 this(context, attrs, 0); 242 } 243 BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)244 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { 245 this(context, attrs, defStyleAttr, 0); 246 } 247 BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)248 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, 249 int defStyleRes) { 250 super(context, attrs, defStyleAttr, defStyleRes); 251 } 252 253 @SuppressLint("ClickableViewAccessibility") 254 @Override onFinishInflate()255 protected void onFinishInflate() { 256 super.onFinishInflate(); 257 mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate( 258 R.layout.bubble_manage_button, this /* parent */, false /* attach */); 259 updateDimensions(); 260 mPointerView = findViewById(R.id.pointer_view); 261 mCurrentPointer = mTopPointer; 262 mPointerView.setVisibility(INVISIBLE); 263 264 // Set {@code TaskView}'s alpha value as zero, since there is no view content to be shown. 265 setContentVisibility(false); 266 267 mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() { 268 @Override 269 public void getOutline(View view, Outline outline) { 270 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 271 } 272 }); 273 mExpandedViewContainer.setClipToOutline(true); 274 mExpandedViewContainer.setLayoutParams( 275 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 276 addView(mExpandedViewContainer); 277 278 // Expanded stack layout, top to bottom: 279 // Expanded view container 280 // ==> bubble row 281 // ==> expanded view 282 // ==> activity view 283 // ==> manage button 284 bringChildToFront(mManageButton); 285 286 applyThemeAttrs(); 287 288 setClipToPadding(false); 289 setOnTouchListener((view, motionEvent) -> { 290 if (mTaskView == null) { 291 return false; 292 } 293 294 final Rect avBounds = new Rect(); 295 mTaskView.getBoundsOnScreen(avBounds); 296 297 // Consume and ignore events on the expanded view padding that are within the 298 // {@code TaskView}'s vertical bounds. These events are part of a back gesture, and so 299 // they should not collapse the stack (which all other touches on areas around the AV 300 // would do). 301 if (motionEvent.getRawY() >= avBounds.top 302 && motionEvent.getRawY() <= avBounds.bottom 303 && (motionEvent.getRawX() < avBounds.left 304 || motionEvent.getRawX() > avBounds.right)) { 305 return true; 306 } 307 308 return false; 309 }); 310 311 // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout 312 // so the Manage button appears on the right. 313 setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 314 } 315 316 /** 317 * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need 318 * to be called after view inflate. 319 */ initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow)320 void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) { 321 mController = controller; 322 mStackView = stackView; 323 mIsOverflow = isOverflow; 324 mPositioner = mController.getPositioner(); 325 326 if (mIsOverflow) { 327 mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( 328 R.layout.bubble_overflow_container, null /* root */); 329 mOverflowView.setBubbleController(mController); 330 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); 331 mExpandedViewContainer.addView(mOverflowView, lp); 332 mExpandedViewContainer.setLayoutParams( 333 new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); 334 bringChildToFront(mOverflowView); 335 mManageButton.setVisibility(GONE); 336 } else { 337 mTaskView = new TaskView(mContext, mController.getTaskOrganizer(), 338 mController.getSyncTransactionQueue()); 339 mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); 340 mExpandedViewContainer.addView(mTaskView); 341 bringChildToFront(mTaskView); 342 } 343 } 344 updateDimensions()345 void updateDimensions() { 346 Resources res = getResources(); 347 updateFontSize(); 348 349 mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); 350 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); 351 mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius); 352 mPointerEffect = new CornerPathEffect(mPointerRadius); 353 mPointerOverlap = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_overlap); 354 mTopPointer = new ShapeDrawable(TriangleShape.create( 355 mPointerWidth, mPointerHeight, true /* pointUp */)); 356 mLeftPointer = new ShapeDrawable(TriangleShape.createHorizontal( 357 mPointerWidth, mPointerHeight, true /* pointLeft */)); 358 mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal( 359 mPointerWidth, mPointerHeight, false /* pointLeft */)); 360 if (mPointerView != null) { 361 updatePointerView(); 362 } 363 364 if (mManageButton != null) { 365 int visibility = mManageButton.getVisibility(); 366 removeView(mManageButton); 367 mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate( 368 R.layout.bubble_manage_button, this /* parent */, false /* attach */); 369 addView(mManageButton); 370 mManageButton.setVisibility(visibility); 371 } 372 } 373 updateFontSize()374 void updateFontSize() { 375 final float fontSize = mContext.getResources() 376 .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material); 377 if (mManageButton != null) { 378 mManageButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize); 379 } 380 if (mOverflowView != null) { 381 mOverflowView.updateFontSize(); 382 } 383 } 384 applyThemeAttrs()385 void applyThemeAttrs() { 386 final TypedArray ta = mContext.obtainStyledAttributes(new int[] { 387 android.R.attr.dialogCornerRadius, 388 android.R.attr.colorBackgroundFloating}); 389 mCornerRadius = ta.getDimensionPixelSize(0, 0); 390 mBackgroundColorFloating = ta.getColor(1, Color.WHITE); 391 mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating); 392 ta.recycle(); 393 394 if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( 395 mContext.getResources())) { 396 mTaskView.setCornerRadius(mCornerRadius); 397 } 398 updatePointerView(); 399 } 400 401 /** Updates the size and visuals of the pointer. **/ updatePointerView()402 private void updatePointerView() { 403 LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); 404 if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { 405 lp.width = mPointerHeight; 406 lp.height = mPointerWidth; 407 } else { 408 lp.width = mPointerWidth; 409 lp.height = mPointerHeight; 410 } 411 mCurrentPointer.setTint(mBackgroundColorFloating); 412 413 Paint arrowPaint = mCurrentPointer.getPaint(); 414 arrowPaint.setColor(mBackgroundColorFloating); 415 arrowPaint.setPathEffect(mPointerEffect); 416 mPointerView.setLayoutParams(lp); 417 mPointerView.setBackground(mCurrentPointer); 418 } 419 getBubbleKey()420 private String getBubbleKey() { 421 return mBubble != null ? mBubble.getKey() : "null"; 422 } 423 424 /** 425 * Sets whether the surface displaying app content should sit on top. This is useful for 426 * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble 427 * being dragged out, the manage menu) this is set to false, otherwise it should be true. 428 */ setSurfaceZOrderedOnTop(boolean onTop)429 void setSurfaceZOrderedOnTop(boolean onTop) { 430 if (mTaskView == null) { 431 return; 432 } 433 mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); 434 } 435 setImeVisible(boolean visible)436 void setImeVisible(boolean visible) { 437 mImeVisible = visible; 438 if (!mImeVisible && mNeedsNewHeight) { 439 updateHeight(); 440 } 441 } 442 443 /** Return a GraphicBuffer with the contents of the task view surface. */ 444 @Nullable snapshotActivitySurface()445 SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() { 446 if (mIsOverflow) { 447 // For now, just snapshot the view and return it as a hw buffer so that the animation 448 // code for both the tasks and overflow can be the same 449 Picture p = new Picture(); 450 mOverflowView.draw( 451 p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight())); 452 p.endRecording(); 453 Bitmap snapshot = Bitmap.createBitmap(p); 454 return new SurfaceControl.ScreenshotHardwareBuffer(snapshot.getHardwareBuffer(), 455 snapshot.getColorSpace(), false /* containsSecureLayers */); 456 } 457 if (mTaskView == null || mTaskView.getSurfaceControl() == null) { 458 return null; 459 } 460 return SurfaceControl.captureLayers( 461 mTaskView.getSurfaceControl(), 462 new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()), 463 1 /* scale */); 464 } 465 getTaskViewLocationOnScreen()466 int[] getTaskViewLocationOnScreen() { 467 if (mIsOverflow) { 468 // This is only used for animating away the surface when switching bubbles, just use the 469 // view location on screen for now to allow us to use the same animation code with tasks 470 return mOverflowView.getLocationOnScreen(); 471 } 472 if (mTaskView != null) { 473 return mTaskView.getLocationOnScreen(); 474 } else { 475 return new int[]{0, 0}; 476 } 477 } 478 479 // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this setManageClickListener(OnClickListener manageClickListener)480 void setManageClickListener(OnClickListener manageClickListener) { 481 mManageButton.setOnClickListener(manageClickListener); 482 } 483 484 /** 485 * Updates the obscured touchable region for the task surface. This calls onLocationChanged, 486 * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is 487 * useful if a view has been added or removed from on top of the {@code TaskView}, such as the 488 * manage menu. 489 */ updateObscuredTouchableRegion()490 void updateObscuredTouchableRegion() { 491 if (mTaskView != null) { 492 mTaskView.onLocationChanged(); 493 } 494 } 495 496 @Override onDetachedFromWindow()497 protected void onDetachedFromWindow() { 498 super.onDetachedFromWindow(); 499 mImeVisible = false; 500 mNeedsNewHeight = false; 501 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 502 Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); 503 } 504 } 505 506 /** 507 * Whether we are currently animating the {@code TaskView}'s alpha value. If this is set to 508 * true, calls to {@link #setContentVisibility} will not be applied until this is set to false 509 * again. 510 */ setAlphaAnimating(boolean animating)511 void setAlphaAnimating(boolean animating) { 512 mIsAlphaAnimating = animating; 513 514 // If we're done animating, apply the correct 515 if (!animating) { 516 setContentVisibility(mIsContentVisible); 517 } 518 } 519 520 /** 521 * Sets the alpha of the underlying {@code TaskView}, since changing the expanded view's alpha 522 * does not affect the {@code TaskView} since it uses a Surface. 523 */ setTaskViewAlpha(float alpha)524 void setTaskViewAlpha(float alpha) { 525 if (mTaskView != null) { 526 mTaskView.setAlpha(alpha); 527 } 528 mPointerView.setAlpha(alpha); 529 setAlpha(alpha); 530 } 531 532 /** 533 * Set visibility of contents in the expanded state. 534 * 535 * @param visibility {@code true} if the contents should be visible on the screen. 536 * 537 * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 538 * and setting {@code false} actually means rendering the contents in transparent. 539 */ setContentVisibility(boolean visibility)540 void setContentVisibility(boolean visibility) { 541 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 542 Log.d(TAG, "setContentVisibility: visibility=" + visibility 543 + " bubble=" + getBubbleKey()); 544 } 545 mIsContentVisible = visibility; 546 if (mTaskView != null && !mIsAlphaAnimating) { 547 mTaskView.setAlpha(visibility ? 1f : 0f); 548 mPointerView.setAlpha(visibility ? 1f : 0f); 549 } 550 } 551 552 @Nullable getTaskView()553 TaskView getTaskView() { 554 return mTaskView; 555 } 556 getTaskId()557 int getTaskId() { 558 return mTaskId; 559 } 560 561 /** 562 * Sets the bubble used to populate this view. 563 */ update(Bubble bubble)564 void update(Bubble bubble) { 565 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 566 Log.d(TAG, "update: bubble=" + bubble); 567 } 568 if (mStackView == null) { 569 Log.w(TAG, "Stack is null for bubble: " + bubble); 570 return; 571 } 572 boolean isNew = mBubble == null || didBackingContentChange(bubble); 573 if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { 574 mBubble = bubble; 575 mManageButton.setContentDescription(getResources().getString( 576 R.string.bubbles_settings_button_description, bubble.getAppName())); 577 mManageButton.setAccessibilityDelegate( 578 new AccessibilityDelegate() { 579 @Override 580 public void onInitializeAccessibilityNodeInfo(View host, 581 AccessibilityNodeInfo info) { 582 super.onInitializeAccessibilityNodeInfo(host, info); 583 // On focus, have TalkBack say 584 // "Actions available. Use swipe up then right to view." 585 // in addition to the default "double tap to activate". 586 mStackView.setupLocalMenu(info); 587 } 588 }); 589 590 if (isNew) { 591 mPendingIntent = mBubble.getBubbleIntent(); 592 if ((mPendingIntent != null || mBubble.hasMetadataShortcutId()) 593 && mTaskView != null) { 594 setContentVisibility(false); 595 mTaskView.setVisibility(VISIBLE); 596 } 597 } 598 applyThemeAttrs(); 599 } else { 600 Log.w(TAG, "Trying to update entry with different key, new bubble: " 601 + bubble.getKey() + " old bubble: " + bubble.getKey()); 602 } 603 } 604 605 /** 606 * Bubbles are backed by a pending intent or a shortcut, once the activity is 607 * started we never change it / restart it on notification updates -- unless the bubbles' 608 * backing data switches. 609 * 610 * This indicates if the new bubble is backed by a different data source than what was 611 * previously shown here (e.g. previously a pending intent & now a shortcut). 612 * 613 * @param newBubble the bubble this view is being updated with. 614 * @return true if the backing content has changed. 615 */ didBackingContentChange(Bubble newBubble)616 private boolean didBackingContentChange(Bubble newBubble) { 617 boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; 618 boolean newIsIntentBased = newBubble.getBubbleIntent() != null; 619 return prevWasIntentBased != newIsIntentBased; 620 } 621 updateHeight()622 void updateHeight() { 623 if (mExpandedViewContainerLocation == null) { 624 return; 625 } 626 627 if ((mBubble != null && mTaskView != null) || mIsOverflow) { 628 float desiredHeight = mPositioner.getExpandedViewHeight(mBubble); 629 int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow); 630 float height = desiredHeight == MAX_HEIGHT 631 ? maxHeight 632 : Math.min(desiredHeight, maxHeight); 633 FrameLayout.LayoutParams lp = mIsOverflow 634 ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams() 635 : (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); 636 mNeedsNewHeight = lp.height != height; 637 if (!mImeVisible) { 638 // If the ime is visible... don't adjust the height because that will cause 639 // a configuration change and the ime will be lost. 640 lp.height = (int) height; 641 if (mIsOverflow) { 642 mOverflowView.setLayoutParams(lp); 643 } else { 644 mTaskView.setLayoutParams(lp); 645 } 646 mNeedsNewHeight = false; 647 } 648 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 649 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() 650 + " height=" + height 651 + " mNeedsNewHeight=" + mNeedsNewHeight); 652 } 653 } 654 } 655 656 /** 657 * Update appearance of the expanded view being displayed. 658 * 659 * @param containerLocationOnScreen The location on-screen of the container the expanded view is 660 * added to. This allows us to calculate max height without 661 * waiting for layout. 662 */ updateView(int[] containerLocationOnScreen)663 public void updateView(int[] containerLocationOnScreen) { 664 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 665 Log.d(TAG, "updateView: bubble=" 666 + getBubbleKey()); 667 } 668 mExpandedViewContainerLocation = containerLocationOnScreen; 669 updateHeight(); 670 if (mTaskView != null 671 && mTaskView.getVisibility() == VISIBLE 672 && mTaskView.isAttachedToWindow()) { 673 mTaskView.onLocationChanged(); 674 } 675 if (mIsOverflow) { 676 mOverflowView.show(); 677 } 678 } 679 680 /** 681 * Sets the position of the pointer. 682 * 683 * When bubbles are showing "vertically" they display along the left / right sides of the 684 * screen with the expanded view beside them. 685 * 686 * If they aren't showing vertically they're positioned along the top of the screen with the 687 * expanded view below them. 688 * 689 * @param bubblePosition the x position of the bubble if showing on top, the y position of 690 * the bubble if showing vertically. 691 * @param onLeft whether the stack was on the left side of the screen when expanded. 692 */ setPointerPosition(float bubblePosition, boolean onLeft, boolean animate)693 public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) { 694 // Pointer gets drawn in the padding 695 final boolean showVertically = mPositioner.showBubblesVertically(); 696 final float paddingLeft = (showVertically && onLeft) 697 ? mPointerHeight - mPointerOverlap 698 : 0; 699 final float paddingRight = (showVertically && !onLeft) 700 ? mPointerHeight - mPointerOverlap 701 : 0; 702 final float paddingTop = showVertically 703 ? 0 704 : mPointerHeight - mPointerOverlap; 705 setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0); 706 707 // Subtract the expandedViewY here because the pointer is placed within the expandedView. 708 float pointerPosition = mPositioner.getPointerPosition(bubblePosition); 709 final float bubbleCenter = mPositioner.showBubblesVertically() 710 ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition) 711 : pointerPosition; 712 // Post because we need the width of the view 713 post(() -> { 714 mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; 715 updatePointerView(); 716 float pointerY; 717 float pointerX; 718 if (showVertically) { 719 pointerY = bubbleCenter - (mPointerWidth / 2f); 720 pointerX = onLeft 721 ? -mPointerHeight + mPointerOverlap 722 : getWidth() - mPaddingRight - mPointerOverlap; 723 } else { 724 pointerY = mPointerOverlap; 725 pointerX = bubbleCenter - (mPointerWidth / 2f); 726 } 727 if (animate) { 728 mPointerView.animate().translationX(pointerX).translationY(pointerY).start(); 729 } else { 730 mPointerView.setTranslationY(pointerY); 731 mPointerView.setTranslationX(pointerX); 732 mPointerView.setVisibility(VISIBLE); 733 } 734 }); 735 } 736 737 /** 738 * Position of the manage button displayed in the expanded view. Used for placing user 739 * education about the manage button. 740 */ getManageButtonBoundsOnScreen(Rect rect)741 public void getManageButtonBoundsOnScreen(Rect rect) { 742 mManageButton.getBoundsOnScreen(rect); 743 } 744 getManageButtonMargin()745 public int getManageButtonMargin() { 746 return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart(); 747 } 748 749 /** 750 * Cleans up anything related to the task and {@code TaskView}. If this view should be reused 751 * after this method is called, then 752 * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first. 753 */ cleanUpExpandedState()754 public void cleanUpExpandedState() { 755 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 756 Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); 757 } 758 if (getTaskId() != INVALID_TASK_ID) { 759 try { 760 ActivityTaskManager.getService().removeTask(getTaskId()); 761 } catch (RemoteException e) { 762 Log.w(TAG, e.getMessage()); 763 } 764 } 765 if (mTaskView != null) { 766 mTaskView.release(); 767 removeView(mTaskView); 768 mTaskView = null; 769 } 770 } 771 772 /** 773 * Description of current expanded view state. 774 */ dump( @onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)775 public void dump( 776 @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) { 777 pw.print("BubbleExpandedView"); 778 pw.print(" taskId: "); pw.println(mTaskId); 779 pw.print(" stackView: "); pw.println(mStackView); 780 } 781 } 782