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