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