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 com.android.wm.shell.animation.Interpolators.ALPHA_IN; 20 import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; 21 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE; 22 23 import android.annotation.Nullable; 24 import android.content.Context; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.graphics.Region; 28 import android.graphics.drawable.ColorDrawable; 29 import android.view.Gravity; 30 import android.view.TouchDelegate; 31 import android.view.View; 32 import android.view.ViewTreeObserver; 33 import android.view.WindowManager; 34 import android.widget.FrameLayout; 35 36 import androidx.annotation.NonNull; 37 38 import com.android.wm.shell.bubbles.Bubble; 39 import com.android.wm.shell.bubbles.BubbleController; 40 import com.android.wm.shell.bubbles.BubbleData; 41 import com.android.wm.shell.bubbles.BubbleOverflow; 42 import com.android.wm.shell.bubbles.BubblePositioner; 43 import com.android.wm.shell.bubbles.BubbleViewProvider; 44 import com.android.wm.shell.bubbles.DeviceConfig; 45 import com.android.wm.shell.bubbles.DismissViewUtils; 46 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; 47 import com.android.wm.shell.common.bubbles.BaseBubblePinController; 48 import com.android.wm.shell.common.bubbles.BubbleBarLocation; 49 import com.android.wm.shell.common.bubbles.DismissView; 50 51 import kotlin.Unit; 52 53 import java.util.Objects; 54 import java.util.function.Consumer; 55 56 /** 57 * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window 58 * manager to display bubbles. However, it is only used when bubbles are being displayed in 59 * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around 60 * on screen and instead shows & animates the expanded bubble for the bubble bar. 61 */ 62 public class BubbleBarLayerView extends FrameLayout 63 implements ViewTreeObserver.OnComputeInternalInsetsListener { 64 65 private static final String TAG = BubbleBarLayerView.class.getSimpleName(); 66 67 private static final float SCRIM_ALPHA = 0.2f; 68 69 private final BubbleController mBubbleController; 70 private final BubbleData mBubbleData; 71 private final BubblePositioner mPositioner; 72 private final BubbleBarAnimationHelper mAnimationHelper; 73 private final BubbleEducationViewController mEducationViewController; 74 private final View mScrimView; 75 private final BubbleExpandedViewPinController mBubbleExpandedViewPinController; 76 77 @Nullable 78 private BubbleViewProvider mExpandedBubble; 79 @Nullable 80 private BubbleBarExpandedView mExpandedView; 81 @Nullable 82 private BubbleBarExpandedViewDragController mDragController; 83 private DismissView mDismissView; 84 private @Nullable Consumer<String> mUnBubbleConversationCallback; 85 86 /** Whether a bubble is expanded. */ 87 private boolean mIsExpanded = false; 88 89 private final Region mTouchableRegion = new Region(); 90 private final Rect mTempRect = new Rect(); 91 92 // Used to ensure touch target size for the menu shown on a bubble expanded view 93 private TouchDelegate mHandleTouchDelegate; 94 private final Rect mHandleTouchBounds = new Rect(); 95 BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData)96 public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData) { 97 super(context); 98 mBubbleController = controller; 99 mBubbleData = bubbleData; 100 mPositioner = mBubbleController.getPositioner(); 101 102 mAnimationHelper = new BubbleBarAnimationHelper(context, 103 this, mPositioner); 104 mEducationViewController = new BubbleEducationViewController(context, (boolean visible) -> { 105 if (mExpandedView == null) return; 106 mExpandedView.setObscured(visible); 107 }); 108 109 mScrimView = new View(getContext()); 110 mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 111 mScrimView.setBackgroundDrawable(new ColorDrawable( 112 getResources().getColor(android.R.color.system_neutral1_1000))); 113 addView(mScrimView); 114 mScrimView.setAlpha(0f); 115 mScrimView.setBackgroundDrawable(new ColorDrawable( 116 getResources().getColor(android.R.color.system_neutral1_1000))); 117 118 setUpDismissView(); 119 120 mBubbleExpandedViewPinController = new BubbleExpandedViewPinController( 121 context, this, mPositioner); 122 mBubbleExpandedViewPinController.setListener( 123 new BaseBubblePinController.LocationChangeListener() { 124 @Override 125 public void onChange(@NonNull BubbleBarLocation bubbleBarLocation) { 126 mBubbleController.animateBubbleBarLocation(bubbleBarLocation); 127 } 128 129 @Override 130 public void onRelease(@NonNull BubbleBarLocation location) { 131 mBubbleController.setBubbleBarLocation(location); 132 } 133 }); 134 135 setOnClickListener(view -> hideModalOrCollapse()); 136 } 137 138 @Override onAttachedToWindow()139 protected void onAttachedToWindow() { 140 super.onAttachedToWindow(); 141 WindowManager windowManager = mContext.getSystemService(WindowManager.class); 142 mPositioner.update(DeviceConfig.create(mContext, Objects.requireNonNull(windowManager))); 143 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 144 } 145 146 @Override onDetachedFromWindow()147 protected void onDetachedFromWindow() { 148 super.onDetachedFromWindow(); 149 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 150 151 if (mExpandedView != null) { 152 mEducationViewController.hideEducation(/* animated = */ false); 153 removeView(mExpandedView); 154 mExpandedView = null; 155 } 156 } 157 158 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)159 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 160 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 161 mTouchableRegion.setEmpty(); 162 getTouchableRegion(mTouchableRegion); 163 inoutInfo.touchableRegion.set(mTouchableRegion); 164 } 165 166 /** Updates the sizes of any displaying expanded view. */ onDisplaySizeChanged()167 public void onDisplaySizeChanged() { 168 if (mIsExpanded && mExpandedView != null) { 169 updateExpandedView(); 170 } 171 } 172 173 /** Whether the stack of bubbles is expanded or not. */ isExpanded()174 public boolean isExpanded() { 175 return mIsExpanded; 176 } 177 178 /** Shows the expanded view of the provided bubble. */ showExpandedView(BubbleViewProvider b)179 public void showExpandedView(BubbleViewProvider b) { 180 BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); 181 if (expandedView == null) { 182 return; 183 } 184 if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) { 185 removeView(mExpandedView); 186 mExpandedView = null; 187 } 188 if (mExpandedView == null) { 189 if (expandedView.getParent() != null) { 190 // Expanded view might be animating collapse and is still attached 191 // Cancel current animations and remove from parent 192 mAnimationHelper.cancelAnimations(); 193 removeView(expandedView); 194 } 195 mExpandedBubble = b; 196 mExpandedView = expandedView; 197 boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY); 198 final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); 199 final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); 200 mExpandedView.setVisibility(GONE); 201 mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); 202 mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight())); 203 mExpandedView.setListener(new BubbleBarExpandedView.Listener() { 204 @Override 205 public void onTaskCreated() { 206 if (mEducationViewController != null && mExpandedView != null) { 207 mEducationViewController.maybeShowManageEducation(b, mExpandedView); 208 } 209 } 210 211 @Override 212 public void onUnBubbleConversation(String bubbleKey) { 213 if (mUnBubbleConversationCallback != null) { 214 mUnBubbleConversationCallback.accept(bubbleKey); 215 } 216 } 217 218 @Override 219 public void onBackPressed() { 220 hideModalOrCollapse(); 221 } 222 }); 223 224 DragListener dragListener = inDismiss -> { 225 if (inDismiss && mExpandedBubble != null) { 226 mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE); 227 } 228 }; 229 mDragController = new BubbleBarExpandedViewDragController( 230 mExpandedView, 231 mDismissView, 232 mAnimationHelper, 233 mPositioner, 234 mBubbleExpandedViewPinController, 235 dragListener); 236 237 addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT)); 238 } 239 240 if (mEducationViewController.isEducationVisible()) { 241 mEducationViewController.hideEducation(/* animated = */ true); 242 } 243 244 mIsExpanded = true; 245 mBubbleController.getSysuiProxy().onStackExpandChanged(true); 246 mAnimationHelper.animateExpansion(mExpandedBubble, () -> { 247 if (mExpandedView == null) return; 248 // Touch delegate for the menu 249 BubbleBarHandleView view = mExpandedView.getHandleView(); 250 view.getBoundsOnScreen(mHandleTouchBounds); 251 // Move top value up to ensure touch target is large enough 252 mHandleTouchBounds.top -= mPositioner.getBubblePaddingTop(); 253 mHandleTouchDelegate = new TouchDelegate(mHandleTouchBounds, 254 mExpandedView.getHandleView()); 255 setTouchDelegate(mHandleTouchDelegate); 256 }); 257 258 showScrim(true); 259 } 260 261 /** Removes the given {@code bubble}. */ removeBubble(Bubble bubble, Runnable endAction)262 public void removeBubble(Bubble bubble, Runnable endAction) { 263 Runnable cleanUp = () -> { 264 bubble.cleanupViews(); 265 endAction.run(); 266 }; 267 if (mBubbleData.getBubbles().isEmpty()) { 268 // we're removing the last bubble. collapse the expanded view and cleanup bubble views 269 // at the end. 270 collapse(cleanUp); 271 } else { 272 cleanUp.run(); 273 } 274 } 275 276 /** Collapses any showing expanded view */ collapse()277 public void collapse() { 278 collapse(/* endAction= */ null); 279 } 280 281 /** 282 * Collapses any showing expanded view. 283 * 284 * @param endAction an action to run and the end of the collapse animation. 285 */ collapse(@ullable Runnable endAction)286 public void collapse(@Nullable Runnable endAction) { 287 if (!mIsExpanded) { 288 if (endAction != null) { 289 endAction.run(); 290 } 291 return; 292 } 293 mIsExpanded = false; 294 final BubbleBarExpandedView viewToRemove = mExpandedView; 295 mEducationViewController.hideEducation(/* animated = */ true); 296 Runnable runnable = () -> { 297 removeView(viewToRemove); 298 if (endAction != null) { 299 endAction.run(); 300 } 301 if (mBubbleData.getBubbles().isEmpty()) { 302 mBubbleController.onAllBubblesAnimatedOut(); 303 } 304 }; 305 if (mDragController != null && mDragController.isStuckToDismiss()) { 306 mAnimationHelper.animateDismiss(runnable); 307 } else { 308 mAnimationHelper.animateCollapse(runnable); 309 } 310 mBubbleController.getSysuiProxy().onStackExpandChanged(false); 311 mExpandedView = null; 312 mDragController = null; 313 setTouchDelegate(null); 314 showScrim(false); 315 } 316 317 /** 318 * Show bubble bar user education relative to the reference position. 319 * @param position the reference position in Screen coordinates. 320 */ showUserEducation(Point position)321 public void showUserEducation(Point position) { 322 mEducationViewController.showStackEducation(position, /* root = */ this, () -> { 323 // When the user education is clicked hide it and expand the selected bubble 324 mEducationViewController.hideEducation(/* animated = */ true, () -> { 325 mBubbleController.expandStackWithSelectedBubble(); 326 return Unit.INSTANCE; 327 }); 328 return Unit.INSTANCE; 329 }); 330 } 331 332 /** Sets the function to call to un-bubble the given conversation. */ setUnBubbleConversationCallback( @ullable Consumer<String> unBubbleConversationCallback)333 public void setUnBubbleConversationCallback( 334 @Nullable Consumer<String> unBubbleConversationCallback) { 335 mUnBubbleConversationCallback = unBubbleConversationCallback; 336 } 337 setUpDismissView()338 private void setUpDismissView() { 339 if (mDismissView != null) { 340 removeView(mDismissView); 341 } 342 mDismissView = new DismissView(getContext()); 343 DismissViewUtils.setup(mDismissView); 344 addView(mDismissView); 345 } 346 347 /** Hides the current modal education/menu view, IME or collapses the expanded view */ hideModalOrCollapse()348 private void hideModalOrCollapse() { 349 if (mEducationViewController.isEducationVisible()) { 350 mEducationViewController.hideEducation(/* animated = */ true); 351 return; 352 } 353 if (isExpanded() && mExpandedView != null) { 354 boolean menuHidden = mExpandedView.hideMenuIfVisible(); 355 if (menuHidden) { 356 return; 357 } 358 boolean imeHidden = mExpandedView.hideImeIfVisible(); 359 if (imeHidden) { 360 return; 361 } 362 } 363 mBubbleController.collapseStack(); 364 } 365 366 /** Updates the expanded view size and position. */ updateExpandedView()367 public void updateExpandedView() { 368 if (mExpandedView == null || mExpandedBubble == null) return; 369 boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY); 370 mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(), 371 isOverflowExpanded, mTempRect); 372 FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams(); 373 lp.width = mTempRect.width(); 374 lp.height = mTempRect.height(); 375 mExpandedView.setLayoutParams(lp); 376 mExpandedView.setX(mTempRect.left); 377 mExpandedView.setY(mTempRect.top); 378 mExpandedView.updateLocation(); 379 } 380 showScrim(boolean show)381 private void showScrim(boolean show) { 382 if (show) { 383 mScrimView.animate() 384 .setInterpolator(ALPHA_IN) 385 .alpha(SCRIM_ALPHA) 386 .start(); 387 } else { 388 mScrimView.animate() 389 .alpha(0f) 390 .setInterpolator(ALPHA_OUT) 391 .start(); 392 } 393 } 394 395 /** 396 * Fills in the touchable region for expanded view. This is used by window manager to 397 * decide which touch events go to the expanded view. 398 */ getTouchableRegion(Region outRegion)399 private void getTouchableRegion(Region outRegion) { 400 mTempRect.setEmpty(); 401 if (mIsExpanded || mEducationViewController.isEducationVisible()) { 402 getBoundsOnScreen(mTempRect); 403 outRegion.op(mTempRect, Region.Op.UNION); 404 } 405 } 406 407 } 408