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 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.graphics.Rect; 25 import android.graphics.Region; 26 import android.graphics.drawable.ColorDrawable; 27 import android.view.TouchDelegate; 28 import android.view.View; 29 import android.view.ViewTreeObserver; 30 import android.widget.FrameLayout; 31 32 import com.android.wm.shell.bubbles.BubbleController; 33 import com.android.wm.shell.bubbles.BubbleOverflow; 34 import com.android.wm.shell.bubbles.BubblePositioner; 35 import com.android.wm.shell.bubbles.BubbleViewProvider; 36 37 import java.util.function.Consumer; 38 39 /** 40 * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window 41 * manager to display bubbles. However, it is only used when bubbles are being displayed in 42 * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around 43 * on screen and instead shows & animates the expanded bubble for the bubble bar. 44 */ 45 public class BubbleBarLayerView extends FrameLayout 46 implements ViewTreeObserver.OnComputeInternalInsetsListener { 47 48 private static final String TAG = BubbleBarLayerView.class.getSimpleName(); 49 50 private static final float SCRIM_ALPHA = 0.2f; 51 52 private final BubbleController mBubbleController; 53 private final BubblePositioner mPositioner; 54 private final BubbleBarAnimationHelper mAnimationHelper; 55 private final BubbleEducationViewController mEducationViewController; 56 private final View mScrimView; 57 58 @Nullable 59 private BubbleViewProvider mExpandedBubble; 60 private BubbleBarExpandedView mExpandedView; 61 private @Nullable Consumer<String> mUnBubbleConversationCallback; 62 63 // TODO(b/273310265) - currently the view is always on the right, need to update for RTL. 64 /** Whether the expanded view is displaying on the left of the screen or not. */ 65 private boolean mOnLeft = false; 66 67 /** Whether a bubble is expanded. */ 68 private boolean mIsExpanded = false; 69 70 private final Region mTouchableRegion = new Region(); 71 private final Rect mTempRect = new Rect(); 72 73 // Used to ensure touch target size for the menu shown on a bubble expanded view 74 private TouchDelegate mHandleTouchDelegate; 75 private final Rect mHandleTouchBounds = new Rect(); 76 BubbleBarLayerView(Context context, BubbleController controller)77 public BubbleBarLayerView(Context context, BubbleController controller) { 78 super(context); 79 mBubbleController = controller; 80 mPositioner = mBubbleController.getPositioner(); 81 82 mAnimationHelper = new BubbleBarAnimationHelper(context, 83 this, mPositioner); 84 mEducationViewController = new BubbleEducationViewController(context, (boolean visible) -> { 85 if (mExpandedView == null) return; 86 mExpandedView.setObscured(visible); 87 }); 88 89 mScrimView = new View(getContext()); 90 mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 91 mScrimView.setBackgroundDrawable(new ColorDrawable( 92 getResources().getColor(android.R.color.system_neutral1_1000))); 93 addView(mScrimView); 94 mScrimView.setAlpha(0f); 95 mScrimView.setBackgroundDrawable(new ColorDrawable( 96 getResources().getColor(android.R.color.system_neutral1_1000))); 97 98 setOnClickListener(view -> hideMenuOrCollapse()); 99 } 100 101 @Override onAttachedToWindow()102 protected void onAttachedToWindow() { 103 super.onAttachedToWindow(); 104 mPositioner.update(); 105 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 106 } 107 108 @Override onDetachedFromWindow()109 protected void onDetachedFromWindow() { 110 super.onDetachedFromWindow(); 111 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 112 113 if (mExpandedView != null) { 114 mEducationViewController.hideManageEducation(/* animated = */ false); 115 removeView(mExpandedView); 116 mExpandedView = null; 117 } 118 } 119 120 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)121 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 122 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 123 mTouchableRegion.setEmpty(); 124 getTouchableRegion(mTouchableRegion); 125 inoutInfo.touchableRegion.set(mTouchableRegion); 126 } 127 128 /** Updates the sizes of any displaying expanded view. */ onDisplaySizeChanged()129 public void onDisplaySizeChanged() { 130 if (mIsExpanded && mExpandedView != null) { 131 updateExpandedView(); 132 } 133 } 134 135 /** Whether the stack of bubbles is expanded or not. */ isExpanded()136 public boolean isExpanded() { 137 return mIsExpanded; 138 } 139 140 // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done. 141 /** Whether the expanded view is positioned on the left or right side of the screen. */ isOnLeft()142 public boolean isOnLeft() { 143 return mOnLeft; 144 } 145 146 /** Shows the expanded view of the provided bubble. */ showExpandedView(BubbleViewProvider b)147 public void showExpandedView(BubbleViewProvider b) { 148 BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); 149 if (expandedView == null) { 150 return; 151 } 152 if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) { 153 removeView(mExpandedView); 154 mExpandedView = null; 155 } 156 if (mExpandedView == null) { 157 if (expandedView.getParent() != null) { 158 // Expanded view might be animating collapse and is still attached 159 // Cancel current animations and remove from parent 160 mAnimationHelper.cancelAnimations(); 161 removeView(expandedView); 162 } 163 mExpandedBubble = b; 164 mExpandedView = expandedView; 165 boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY); 166 final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); 167 final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); 168 mExpandedView.setVisibility(GONE); 169 mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); 170 mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight())); 171 mExpandedView.setListener(new BubbleBarExpandedView.Listener() { 172 @Override 173 public void onTaskCreated() { 174 mEducationViewController.maybeShowManageEducation(b, mExpandedView); 175 } 176 177 @Override 178 public void onUnBubbleConversation(String bubbleKey) { 179 if (mUnBubbleConversationCallback != null) { 180 mUnBubbleConversationCallback.accept(bubbleKey); 181 } 182 } 183 184 @Override 185 public void onBackPressed() { 186 hideMenuOrCollapse(); 187 } 188 }); 189 190 addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); 191 } 192 193 mIsExpanded = true; 194 mBubbleController.getSysuiProxy().onStackExpandChanged(true); 195 mAnimationHelper.animateExpansion(mExpandedBubble, () -> { 196 if (mExpandedView == null) return; 197 // Touch delegate for the menu 198 BubbleBarHandleView view = mExpandedView.getHandleView(); 199 view.getBoundsOnScreen(mHandleTouchBounds); 200 mHandleTouchBounds.top -= mPositioner.getBubblePaddingTop(); 201 mHandleTouchDelegate = new TouchDelegate(mHandleTouchBounds, 202 mExpandedView.getHandleView()); 203 setTouchDelegate(mHandleTouchDelegate); 204 }); 205 206 showScrim(true); 207 } 208 209 /** Collapses any showing expanded view */ collapse()210 public void collapse() { 211 mIsExpanded = false; 212 final BubbleBarExpandedView viewToRemove = mExpandedView; 213 mEducationViewController.hideManageEducation(/* animated = */ true); 214 mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); 215 mBubbleController.getSysuiProxy().onStackExpandChanged(false); 216 mExpandedView = null; 217 setTouchDelegate(null); 218 showScrim(false); 219 } 220 221 /** Sets the function to call to un-bubble the given conversation. */ setUnBubbleConversationCallback( @ullable Consumer<String> unBubbleConversationCallback)222 public void setUnBubbleConversationCallback( 223 @Nullable Consumer<String> unBubbleConversationCallback) { 224 mUnBubbleConversationCallback = unBubbleConversationCallback; 225 } 226 227 /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */ hideMenuOrCollapse()228 private void hideMenuOrCollapse() { 229 if (mEducationViewController.isManageEducationVisible()) { 230 mEducationViewController.hideManageEducation(/* animated = */ true); 231 } else if (isExpanded() && mExpandedView != null) { 232 mExpandedView.hideMenuOrCollapse(); 233 } else { 234 mBubbleController.collapseStack(); 235 } 236 } 237 238 /** Updates the expanded view size and position. */ updateExpandedView()239 private void updateExpandedView() { 240 if (mExpandedView == null) return; 241 boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY); 242 final int padding = mPositioner.getBubbleBarExpandedViewPadding(); 243 final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded); 244 final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded); 245 FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams(); 246 lp.width = width; 247 lp.height = height; 248 mExpandedView.setLayoutParams(lp); 249 if (mOnLeft) { 250 mExpandedView.setX(mPositioner.getInsets().left + padding); 251 } else { 252 mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding); 253 } 254 mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height); 255 mExpandedView.updateLocation(); 256 } 257 showScrim(boolean show)258 private void showScrim(boolean show) { 259 if (show) { 260 mScrimView.animate() 261 .setInterpolator(ALPHA_IN) 262 .alpha(SCRIM_ALPHA) 263 .start(); 264 } else { 265 mScrimView.animate() 266 .alpha(0f) 267 .setInterpolator(ALPHA_OUT) 268 .start(); 269 } 270 } 271 272 /** 273 * Fills in the touchable region for expanded view. This is used by window manager to 274 * decide which touch events go to the expanded view. 275 */ getTouchableRegion(Region outRegion)276 private void getTouchableRegion(Region outRegion) { 277 mTempRect.setEmpty(); 278 if (mIsExpanded) { 279 getBoundsOnScreen(mTempRect); 280 outRegion.op(mTempRect, Region.Op.UNION); 281 } 282 } 283 } 284