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