1 /* 2 * Copyright (C) 2023 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.bar; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 21 import android.annotation.Nullable; 22 import android.content.Context; 23 import android.graphics.Insets; 24 import android.graphics.Outline; 25 import android.graphics.Rect; 26 import android.os.Bundle; 27 import android.util.AttributeSet; 28 import android.util.FloatProperty; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewOutlineProvider; 33 import android.view.accessibility.AccessibilityNodeInfo; 34 import android.widget.FrameLayout; 35 36 import androidx.annotation.NonNull; 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.wm.shell.R; 40 import com.android.wm.shell.bubbles.Bubble; 41 import com.android.wm.shell.bubbles.BubbleExpandedViewManager; 42 import com.android.wm.shell.bubbles.BubbleLogger; 43 import com.android.wm.shell.bubbles.BubbleOverflowContainerView; 44 import com.android.wm.shell.bubbles.BubblePositioner; 45 import com.android.wm.shell.bubbles.BubbleTaskView; 46 import com.android.wm.shell.bubbles.BubbleTaskViewListener; 47 import com.android.wm.shell.bubbles.Bubbles; 48 import com.android.wm.shell.bubbles.RegionSamplingProvider; 49 import com.android.wm.shell.dagger.HasWMComponent; 50 import com.android.wm.shell.shared.bubbles.BubbleBarLocation; 51 import com.android.wm.shell.shared.handles.RegionSamplingHelper; 52 import com.android.wm.shell.taskview.TaskView; 53 54 import java.util.concurrent.Executor; 55 import java.util.function.Supplier; 56 57 import javax.inject.Inject; 58 59 /** Expanded view of a bubble when it's part of the bubble bar. */ 60 public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewListener.Callback { 61 /** 62 * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal 63 * actions and events 64 */ 65 public interface Listener { 66 /** Called when the task view task is first created. */ onTaskCreated()67 void onTaskCreated(); 68 /** Called when expanded view needs to un-bubble the given conversation */ onUnBubbleConversation(String bubbleKey)69 void onUnBubbleConversation(String bubbleKey); 70 /** Called when expanded view task view back button pressed */ onBackPressed()71 void onBackPressed(); 72 } 73 74 /** 75 * A property wrapper around corner radius for the expanded view, handled by 76 * {@link #setCornerRadius(float)} and {@link #getCornerRadius()} methods. 77 */ 78 public static final FloatProperty<BubbleBarExpandedView> CORNER_RADIUS = new FloatProperty<>( 79 "cornerRadius") { 80 @Override 81 public void setValue(BubbleBarExpandedView bbev, float radius) { 82 bbev.setCornerRadius(radius); 83 } 84 85 @Override 86 public Float get(BubbleBarExpandedView bbev) { 87 return bbev.getCornerRadius(); 88 } 89 }; 90 91 /** 92 * Property to set alpha for the task view 93 */ 94 public static final FloatProperty<BubbleBarExpandedView> TASK_VIEW_ALPHA = new FloatProperty<>( 95 "taskViewAlpha") { 96 @Override 97 public void setValue(BubbleBarExpandedView bbev, float alpha) { 98 bbev.setTaskViewAlpha(alpha); 99 } 100 101 @Override 102 public Float get(BubbleBarExpandedView bbev) { 103 return bbev.mTaskView != null ? bbev.mTaskView.getAlpha() : bbev.getAlpha(); 104 } 105 }; 106 107 private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); 108 private static final int INVALID_TASK_ID = -1; 109 110 private Bubble mBubble; 111 private BubbleExpandedViewManager mManager; 112 private BubblePositioner mPositioner; 113 private boolean mIsOverflow; 114 private BubbleTaskViewListener mBubbleTaskViewListener; 115 private BubbleBarMenuViewController mMenuViewController; 116 @Nullable 117 private Supplier<Rect> mLayerBoundsSupplier; 118 @Nullable 119 private Listener mListener; 120 121 private BubbleBarHandleView mHandleView; 122 @Nullable 123 private TaskView mTaskView; 124 @Nullable 125 private BubbleOverflowContainerView mOverflowView; 126 127 /** 128 * The handle shown in the caption area is tinted based on the background color of the area. 129 * This can vary so we sample the caption region and update the handle color based on that. 130 * If we're showing the overflow, the helper and executors will be null. 131 */ 132 @Nullable 133 private RegionSamplingHelper mRegionSamplingHelper; 134 @Nullable 135 private RegionSamplingProvider mRegionSamplingProvider; 136 @Nullable 137 private Executor mMainExecutor; 138 @Nullable 139 private Executor mBackgroundExecutor; 140 private final Rect mSampleRect = new Rect(); 141 private final int[] mLoc = new int[2]; 142 private final Rect mTempBounds = new Rect(); 143 144 /** Height of the caption inset at the top of the TaskView */ 145 private int mCaptionHeight; 146 /** Corner radius used when view is resting */ 147 private float mRestingCornerRadius = 0f; 148 /** Corner radius applied while dragging */ 149 private float mDraggedCornerRadius = 0f; 150 /** Current corner radius */ 151 private float mCurrentCornerRadius = 0f; 152 153 /** A runnable to start the expansion animation as soon as the task view is made visible. */ 154 @Nullable 155 private Runnable mAnimateExpansion = null; 156 private TaskViewVisibilityState mVisibilityState = TaskViewVisibilityState.INVISIBLE; 157 158 /** 159 * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If 160 * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha 161 * value until the animation ends. 162 */ 163 private boolean mIsContentVisible = false; 164 private boolean mIsAnimating; 165 private boolean mIsDragging; 166 167 private boolean mIsClipping = false; 168 private int mBottomClip = 0; 169 170 /** An enum value that tracks the visibility state of the task view */ 171 private enum TaskViewVisibilityState { 172 /** The task view is going away, and we're waiting for the surface to be destroyed. */ 173 PENDING_INVISIBLE, 174 /** The task view is invisible and does not have a surface. */ 175 INVISIBLE, 176 /** The task view is in the process of being added to a surface. */ 177 PENDING_VISIBLE, 178 /** The task view is visible and has a surface. */ 179 VISIBLE 180 } 181 182 // Ideally this would be package private, but we have to set this in a fake for test and we 183 // don't yet have dagger set up for tests, so have to set manually 184 @VisibleForTesting 185 @Inject 186 public BubbleLogger bubbleLogger; 187 BubbleBarExpandedView(Context context)188 public BubbleBarExpandedView(Context context) { 189 this(context, null); 190 } 191 BubbleBarExpandedView(Context context, AttributeSet attrs)192 public BubbleBarExpandedView(Context context, AttributeSet attrs) { 193 this(context, attrs, 0); 194 } 195 BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr)196 public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { 197 this(context, attrs, defStyleAttr, 0); 198 } 199 BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)200 public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr, 201 int defStyleRes) { 202 super(context, attrs, defStyleAttr, defStyleRes); 203 } 204 205 @Override onFinishInflate()206 protected void onFinishInflate() { 207 super.onFinishInflate(); 208 Context context = getContext(); 209 if (context instanceof HasWMComponent) { 210 ((HasWMComponent) context).getWMComponent().inject(this); 211 } 212 setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation)); 213 mCaptionHeight = context.getResources().getDimensionPixelSize( 214 R.dimen.bubble_bar_expanded_view_caption_height); 215 mHandleView = findViewById(R.id.bubble_bar_handle_view); 216 applyThemeAttrs(); 217 setClipToOutline(true); 218 setOutlineProvider(new ViewOutlineProvider() { 219 @Override 220 public void getOutline(View view, Outline outline) { 221 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight() - mBottomClip, 222 mCurrentCornerRadius); 223 } 224 }); 225 // Set a touch sink to ensure that clicks on the caption area do not propagate to the parent 226 setOnTouchListener((v, event) -> true); 227 } 228 229 /** Initializes the view, must be called before doing anything else. */ initialize(BubbleExpandedViewManager expandedViewManager, BubblePositioner positioner, boolean isOverflow, @Nullable BubbleTaskView bubbleTaskView, @Nullable Executor mainExecutor, @Nullable Executor backgroundExecutor, @Nullable RegionSamplingProvider regionSamplingProvider)230 public void initialize(BubbleExpandedViewManager expandedViewManager, 231 BubblePositioner positioner, 232 boolean isOverflow, 233 @Nullable BubbleTaskView bubbleTaskView, 234 @Nullable Executor mainExecutor, 235 @Nullable Executor backgroundExecutor, 236 @Nullable RegionSamplingProvider regionSamplingProvider) { 237 mManager = expandedViewManager; 238 mPositioner = positioner; 239 mIsOverflow = isOverflow; 240 mMainExecutor = mainExecutor; 241 mBackgroundExecutor = backgroundExecutor; 242 mRegionSamplingProvider = regionSamplingProvider; 243 244 if (mIsOverflow) { 245 mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( 246 R.layout.bubble_overflow_container, null /* root */); 247 mOverflowView.initialize(expandedViewManager, positioner); 248 addView(mOverflowView); 249 // Don't show handle for overflow 250 mHandleView.setVisibility(View.GONE); 251 } else { 252 mTaskView = bubbleTaskView.getTaskView(); 253 mBubbleTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView, 254 /* viewParent= */ this, 255 expandedViewManager, 256 /* callback= */ this); 257 258 // if the task view is already attached to a parent we need to remove it 259 if (mTaskView.getParent() != null) { 260 // it's possible that the task view is visible, e.g. if we're unfolding, in which 261 // case removing it will trigger a visibility change. we have to wait for that 262 // signal before we can add it to this expanded view, otherwise the signal will be 263 // incorrect because the task view will have a surface. 264 // if the task view is not visible, then it has no surface and removing it will not 265 // trigger any visibility change signals. 266 if (bubbleTaskView.isVisible()) { 267 mVisibilityState = TaskViewVisibilityState.PENDING_INVISIBLE; 268 } 269 ((ViewGroup) mTaskView.getParent()).removeView(mTaskView); 270 } 271 272 // if we're invisible it's safe to setup the task view and then await on the visibility 273 // signal. 274 if (mVisibilityState == TaskViewVisibilityState.INVISIBLE) { 275 mVisibilityState = TaskViewVisibilityState.PENDING_VISIBLE; 276 setupTaskView(); 277 } 278 279 // Handle view needs to draw on top of task view. 280 bringChildToFront(mHandleView); 281 282 mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate()); 283 } 284 mMenuViewController = new BubbleBarMenuViewController(mContext, mHandleView, this); 285 mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { 286 @Override 287 public void onMenuVisibilityChanged(boolean visible) { 288 setObscured(visible); 289 if (visible) { 290 mHandleView.setFocusable(false); 291 mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 292 } else { 293 mHandleView.setFocusable(true); 294 mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO); 295 } 296 } 297 298 @Override 299 public void onUnBubbleConversation(Bubble bubble) { 300 if (mListener != null) { 301 mListener.onUnBubbleConversation(bubble.getKey()); 302 } 303 bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_OPT_OUT); 304 } 305 306 @Override 307 public void onOpenAppSettings(Bubble bubble) { 308 mManager.collapseStack(); 309 mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser()); 310 bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_GO_TO_SETTINGS); 311 } 312 313 @Override 314 public void onDismissBubble(Bubble bubble) { 315 mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); 316 bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU); 317 } 318 319 @Override 320 public void onMoveToFullscreen(Bubble bubble) { 321 if (mTaskView != null) { 322 mTaskView.moveToFullscreen(); 323 } 324 } 325 }); 326 mHandleView.setOnClickListener(view -> { 327 mMenuViewController.showMenu(true /* animated */); 328 }); 329 } 330 setupTaskView()331 private void setupTaskView() { 332 FrameLayout.LayoutParams lp = 333 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); 334 addView(mTaskView, lp); 335 mTaskView.setEnableSurfaceClipping(true); 336 mTaskView.setCornerRadius(mCurrentCornerRadius); 337 mTaskView.setVisibility(VISIBLE); 338 mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); 339 } 340 getHandleView()341 public BubbleBarHandleView getHandleView() { 342 return mHandleView; 343 } 344 345 /** Updates the view based on the current theme. */ applyThemeAttrs()346 public void applyThemeAttrs() { 347 mCaptionHeight = getResources().getDimensionPixelSize( 348 R.dimen.bubble_bar_expanded_view_caption_height); 349 mRestingCornerRadius = getResources().getDimensionPixelSize( 350 R.dimen.bubble_bar_expanded_view_corner_radius); 351 mDraggedCornerRadius = getResources().getDimensionPixelSize( 352 R.dimen.bubble_bar_expanded_view_corner_radius_dragged); 353 354 mCurrentCornerRadius = mRestingCornerRadius; 355 356 if (mTaskView != null) { 357 mTaskView.setCornerRadius(mCurrentCornerRadius); 358 mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); 359 } 360 } 361 362 @Override onDetachedFromWindow()363 protected void onDetachedFromWindow() { 364 super.onDetachedFromWindow(); 365 // Hide manage menu when view disappears 366 mMenuViewController.hideMenu(false /* animated */); 367 if (mRegionSamplingHelper != null) { 368 mRegionSamplingHelper.stopAndDestroy(); 369 } 370 } 371 372 @Override onAttachedToWindow()373 protected void onAttachedToWindow() { 374 super.onAttachedToWindow(); 375 recreateRegionSamplingHelper(); 376 } 377 378 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)379 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 380 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 381 if (mTaskView != null) { 382 int height = MeasureSpec.getSize(heightMeasureSpec); 383 measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, 384 MeasureSpec.getMode(heightMeasureSpec))); 385 } 386 } 387 388 @Override onLayout(boolean changed, int l, int t, int r, int b)389 protected void onLayout(boolean changed, int l, int t, int r, int b) { 390 super.onLayout(changed, l, t, r, b); 391 if (mTaskView != null) { 392 mTaskView.layout(l, t, r, t + mTaskView.getMeasuredHeight()); 393 } 394 } 395 396 @Override onTaskCreated()397 public void onTaskCreated() { 398 if (mTaskView != null) { 399 mTaskView.setAlpha(0); 400 } 401 if (mListener != null) { 402 mListener.onTaskCreated(); 403 } 404 // when the task is created we're visible 405 onTaskViewVisible(); 406 } 407 408 @Override onContentVisibilityChanged(boolean visible)409 public void onContentVisibilityChanged(boolean visible) { 410 if (mVisibilityState == TaskViewVisibilityState.PENDING_INVISIBLE && !visible) { 411 // the surface is now destroyed. set up the task view and wait for the visibility 412 // signal. 413 mVisibilityState = TaskViewVisibilityState.PENDING_VISIBLE; 414 setupTaskView(); 415 return; 416 } 417 if (visible) { 418 onTaskViewVisible(); 419 } 420 } 421 422 @Override onTaskRemovalStarted()423 public void onTaskRemovalStarted() { 424 if (mRegionSamplingHelper != null) { 425 mRegionSamplingHelper.stopAndDestroy(); 426 } 427 } 428 429 @Override onBackPressed()430 public void onBackPressed() { 431 if (mListener == null) return; 432 mListener.onBackPressed(); 433 } 434 animateExpansionWhenTaskViewVisible(Runnable animateExpansion)435 void animateExpansionWhenTaskViewVisible(Runnable animateExpansion) { 436 if (mVisibilityState == TaskViewVisibilityState.VISIBLE || mIsOverflow) { 437 animateExpansion.run(); 438 } else { 439 mAnimateExpansion = animateExpansion; 440 } 441 } 442 onTaskViewVisible()443 private void onTaskViewVisible() { 444 // if we're waiting to be visible, start the expansion animation if it's pending. 445 if (mVisibilityState == TaskViewVisibilityState.PENDING_VISIBLE) { 446 mVisibilityState = TaskViewVisibilityState.VISIBLE; 447 if (mAnimateExpansion != null) { 448 mAnimateExpansion.run(); 449 mAnimateExpansion = null; 450 } 451 } 452 } 453 454 /** 455 * Set whether this view is currently being dragged. 456 * 457 * When dragging, the handle is hidden and content shouldn't be sampled. When dragging has 458 * ended we should start again. 459 */ setDragging(boolean isDragging)460 public void setDragging(boolean isDragging) { 461 if (isDragging != mIsDragging) { 462 mIsDragging = isDragging; 463 updateSamplingState(); 464 465 if (isDragging && mPositioner.isImeVisible()) { 466 // Hide the IME when dragging begins 467 mManager.hideCurrentInputMethod(); 468 } 469 } 470 } 471 472 /** Returns whether region sampling should be enabled, i.e. if task view content is visible. */ shouldSampleRegion()473 private boolean shouldSampleRegion() { 474 return mTaskView != null 475 && mTaskView.getTaskInfo() != null 476 && !mIsDragging 477 && !mIsAnimating 478 && mIsContentVisible; 479 } 480 481 /** 482 * Handles starting or stopping the region sampling helper based on 483 * {@link #shouldSampleRegion()}. 484 */ updateSamplingState()485 private void updateSamplingState() { 486 if (mRegionSamplingHelper == null) return; 487 boolean shouldSample = shouldSampleRegion(); 488 if (shouldSample) { 489 mRegionSamplingHelper.start(getCaptionSampleRect()); 490 } else { 491 mRegionSamplingHelper.stop(); 492 } 493 } 494 495 /** Returns the current area of the caption bar, in screen coordinates. */ getCaptionSampleRect()496 Rect getCaptionSampleRect() { 497 if (mTaskView == null) return null; 498 mTaskView.getLocationOnScreen(mLoc); 499 mSampleRect.set(mLoc[0], mLoc[1], 500 mLoc[0] + mTaskView.getWidth(), 501 mLoc[1] + mCaptionHeight); 502 return mSampleRect; 503 } 504 505 @VisibleForTesting 506 @Nullable getRegionSamplingHelper()507 public RegionSamplingHelper getRegionSamplingHelper() { 508 return mRegionSamplingHelper; 509 } 510 511 /** Cleans up the expanded view, should be called when the bubble is no longer active. */ cleanUpExpandedState()512 public void cleanUpExpandedState() { 513 mMenuViewController.hideMenu(false /* animated */); 514 } 515 516 /** 517 * Hides the current modal menu if it is visible 518 * @return {@code true} if menu was visible and is hidden 519 */ hideMenuIfVisible()520 public boolean hideMenuIfVisible() { 521 if (mMenuViewController.isMenuVisible()) { 522 mMenuViewController.hideMenu(true /* animated */); 523 return true; 524 } 525 return false; 526 } 527 528 /** 529 * Hides the IME if it is visible 530 * @return {@code true} if IME was visible 531 */ hideImeIfVisible()532 public boolean hideImeIfVisible() { 533 if (mPositioner.isImeVisible()) { 534 mManager.hideCurrentInputMethod(); 535 return true; 536 } 537 return false; 538 } 539 540 /** Updates the bubble shown in the expanded view. */ update(Bubble bubble)541 public void update(Bubble bubble) { 542 mBubble = bubble; 543 mBubbleTaskViewListener.setBubble(bubble); 544 mMenuViewController.updateMenu(bubble); 545 } 546 547 /** The task id of the activity shown in the task view, if it exists. */ getTaskId()548 public int getTaskId() { 549 return mBubbleTaskViewListener != null 550 ? mBubbleTaskViewListener.getTaskId() 551 : INVALID_TASK_ID; 552 } 553 554 /** Sets layer bounds supplier used for obscured touchable region of task view */ setLayerBoundsSupplier(@ullable Supplier<Rect> supplier)555 void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) { 556 mLayerBoundsSupplier = supplier; 557 } 558 559 /** Sets expanded view listener */ setListener(@ullable Listener listener)560 void setListener(@Nullable Listener listener) { 561 mListener = listener; 562 } 563 564 /** Sets whether the view is obscured by some modal view */ setObscured(boolean obscured)565 void setObscured(boolean obscured) { 566 if (mTaskView == null || mLayerBoundsSupplier == null) return; 567 // Updates the obscured touchable region for the task surface. 568 mTaskView.setObscuredTouchRect(obscured ? mLayerBoundsSupplier.get() : null); 569 } 570 571 /** 572 * Call when the location or size of the view has changed to update TaskView. 573 */ updateLocation()574 public void updateLocation() { 575 if (mTaskView != null) { 576 mTaskView.onLocationChanged(); 577 } 578 } 579 580 /** Shows the expanded view for the overflow if it exists. */ maybeShowOverflow()581 void maybeShowOverflow() { 582 if (mOverflowView != null) { 583 // post this to the looper so that the view has a chance to be laid out before it can 584 // calculate row and column sizes correctly. 585 post(() -> mOverflowView.show()); 586 } 587 } 588 589 /** Sets the alpha of the task view. */ setContentVisibility(boolean visible)590 public void setContentVisibility(boolean visible) { 591 mIsContentVisible = visible; 592 593 if (mTaskView == null) return; 594 595 if (!mIsAnimating) { 596 mTaskView.setAlpha(visible ? 1f : 0f); 597 if (mRegionSamplingHelper != null) { 598 mRegionSamplingHelper.setWindowVisible(visible); 599 } 600 updateSamplingState(); 601 } 602 } 603 604 /** 605 * Sets the alpha of both this view and the task view. 606 */ setTaskViewAlpha(float alpha)607 public void setTaskViewAlpha(float alpha) { 608 if (mTaskView != null) { 609 mTaskView.setAlpha(alpha); 610 } 611 setAlpha(alpha); 612 } 613 614 /** 615 * Sets whether the surface displaying app content should sit on top. This is useful for 616 * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble 617 * being dragged out, the manage menu) this is set to false, otherwise it should be true. 618 */ setSurfaceZOrderedOnTop(boolean onTop)619 public void setSurfaceZOrderedOnTop(boolean onTop) { 620 if (mTaskView == null) { 621 return; 622 } 623 mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); 624 } 625 626 @VisibleForTesting isSurfaceZOrderedOnTop()627 boolean isSurfaceZOrderedOnTop() { 628 return mTaskView != null && mTaskView.isZOrderedOnTop(); 629 } 630 631 /** 632 * Sets whether the view is animating, in this case we won't change the content visibility 633 * until the animation is done. 634 */ setAnimating(boolean animating)635 public void setAnimating(boolean animating) { 636 mIsAnimating = animating; 637 if (mIsAnimating) { 638 // Stop sampling while animating -- when animating is done setContentVisibility will 639 // re-trigger sampling if we're visible. 640 updateSamplingState(); 641 } 642 // If we're done animating, apply the correct visibility. 643 if (!animating) { 644 setContentVisibility(mIsContentVisible); 645 } 646 } 647 648 /** 649 * Check whether the view is animating 650 */ isAnimating()651 public boolean isAnimating() { 652 return mIsAnimating; 653 } 654 655 /** @return corner radius that should be applied while view is in rest */ getRestingCornerRadius()656 public float getRestingCornerRadius() { 657 return mRestingCornerRadius; 658 } 659 660 /** @return corner radius that should be applied while view is being dragged */ getDraggedCornerRadius()661 public float getDraggedCornerRadius() { 662 return mDraggedCornerRadius; 663 } 664 665 /** @return current corner radius */ getCornerRadius()666 public float getCornerRadius() { 667 return mCurrentCornerRadius; 668 } 669 670 /** Update corner radius */ setCornerRadius(float cornerRadius)671 public void setCornerRadius(float cornerRadius) { 672 if (mCurrentCornerRadius != cornerRadius) { 673 mCurrentCornerRadius = cornerRadius; 674 if (mTaskView != null) { 675 mTaskView.setCornerRadius(cornerRadius); 676 } 677 invalidateOutline(); 678 } 679 } 680 681 /** The y coordinate of the bottom of the expanded view. */ getContentBottomOnScreen()682 public int getContentBottomOnScreen() { 683 if (mOverflowView != null) { 684 mOverflowView.getBoundsOnScreen(mTempBounds); 685 } 686 if (mTaskView != null) { 687 mTaskView.getBoundsOnScreen(mTempBounds); 688 } 689 return mTempBounds.bottom; 690 } 691 692 /** Update the amount by which to clip the expanded view at the bottom. */ updateBottomClip(int bottomClip)693 public void updateBottomClip(int bottomClip) { 694 mBottomClip = bottomClip; 695 onClipUpdate(); 696 } 697 onClipUpdate()698 private void onClipUpdate() { 699 if (mBottomClip == 0) { 700 if (mIsClipping) { 701 mIsClipping = false; 702 if (mTaskView != null) { 703 mTaskView.setClipBounds(null); 704 mTaskView.setEnableSurfaceClipping(false); 705 } 706 invalidateOutline(); 707 } 708 } else { 709 if (!mIsClipping) { 710 mIsClipping = true; 711 if (mTaskView != null) { 712 mTaskView.setEnableSurfaceClipping(true); 713 } 714 } 715 invalidateOutline(); 716 if (mTaskView != null) { 717 Rect clipBounds = new Rect(0, 0, 718 mTaskView.getWidth(), 719 mTaskView.getHeight() - mBottomClip); 720 mTaskView.setClipBounds(clipBounds); 721 } 722 } 723 } 724 recreateRegionSamplingHelper()725 private void recreateRegionSamplingHelper() { 726 if (mRegionSamplingHelper != null) { 727 mRegionSamplingHelper.stopAndDestroy(); 728 } 729 if (mMainExecutor == null || mBackgroundExecutor == null 730 || mRegionSamplingProvider == null) { 731 // Null when it's the overflow / don't need sampling then. 732 return; 733 } 734 mRegionSamplingHelper = mRegionSamplingProvider.createHelper(this, 735 new RegionSamplingHelper.SamplingCallback() { 736 @Override 737 public void onRegionDarknessChanged(boolean isRegionDark) { 738 if (mHandleView != null) { 739 mHandleView.updateHandleColor(isRegionDark, 740 true /* animated */); 741 } 742 } 743 744 @Override 745 public Rect getSampledRegion(View sampledView) { 746 return getCaptionSampleRect(); 747 } 748 749 @Override 750 public boolean isSamplingEnabled() { 751 return shouldSampleRegion(); 752 } 753 }, mMainExecutor, mBackgroundExecutor); 754 } 755 756 private class HandleViewAccessibilityDelegate extends AccessibilityDelegate { 757 @Override onInitializeAccessibilityNodeInfo(@onNull View host, @NonNull AccessibilityNodeInfo info)758 public void onInitializeAccessibilityNodeInfo(@NonNull View host, 759 @NonNull AccessibilityNodeInfo info) { 760 super.onInitializeAccessibilityNodeInfo(host, info); 761 info.addAction(new AccessibilityNodeInfo.AccessibilityAction( 762 AccessibilityNodeInfo.ACTION_CLICK, getResources().getString( 763 R.string.bubble_accessibility_action_expand_menu))); 764 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE); 765 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); 766 if (mPositioner.isBubbleBarOnLeft()) { 767 info.addAction(new AccessibilityNodeInfo.AccessibilityAction( 768 R.id.action_move_bubble_bar_right, getResources().getString( 769 R.string.bubble_accessibility_action_move_bar_right))); 770 } else { 771 info.addAction(new AccessibilityNodeInfo.AccessibilityAction( 772 R.id.action_move_bubble_bar_left, getResources().getString( 773 R.string.bubble_accessibility_action_move_bar_left))); 774 } 775 } 776 777 @Override performAccessibilityAction(@onNull View host, int action, @Nullable Bundle args)778 public boolean performAccessibilityAction(@NonNull View host, int action, 779 @Nullable Bundle args) { 780 if (super.performAccessibilityAction(host, action, args)) { 781 return true; 782 } 783 if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) { 784 mManager.collapseStack(); 785 return true; 786 } 787 if (action == AccessibilityNodeInfo.ACTION_DISMISS) { 788 mManager.dismissBubble(mBubble, Bubbles.DISMISS_USER_GESTURE); 789 return true; 790 } 791 if (action == R.id.action_move_bubble_bar_left) { 792 mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT, 793 BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW); 794 return true; 795 } 796 if (action == R.id.action_move_bubble_bar_right) { 797 mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT, 798 BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW); 799 return true; 800 } 801 return false; 802 } 803 } 804 } 805