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