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 android.annotation.Nullable; 20 import android.app.ActivityManager; 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.Color; 24 import android.graphics.Insets; 25 import android.graphics.Outline; 26 import android.graphics.Rect; 27 import android.util.AttributeSet; 28 import android.view.LayoutInflater; 29 import android.view.View; 30 import android.view.ViewOutlineProvider; 31 import android.widget.FrameLayout; 32 33 import com.android.internal.policy.ScreenDecorationsUtils; 34 import com.android.wm.shell.R; 35 import com.android.wm.shell.bubbles.Bubble; 36 import com.android.wm.shell.bubbles.BubbleController; 37 import com.android.wm.shell.bubbles.BubbleOverflowContainerView; 38 import com.android.wm.shell.bubbles.BubbleTaskViewHelper; 39 import com.android.wm.shell.bubbles.Bubbles; 40 import com.android.wm.shell.taskview.TaskView; 41 42 import java.util.function.Supplier; 43 44 /** 45 * Expanded view of a bubble when it's part of the bubble bar. 46 * 47 * {@link BubbleController#isShowingAsBubbleBar()} 48 */ 49 public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener { 50 /** 51 * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal 52 * actions and events 53 */ 54 public interface Listener { 55 /** Called when the task view task is first created. */ onTaskCreated()56 void onTaskCreated(); 57 /** Called when expanded view needs to un-bubble the given conversation */ onUnBubbleConversation(String bubbleKey)58 void onUnBubbleConversation(String bubbleKey); 59 /** Called when expanded view task view back button pressed */ onBackPressed()60 void onBackPressed(); 61 } 62 63 private static final String TAG = BubbleBarExpandedView.class.getSimpleName(); 64 private static final int INVALID_TASK_ID = -1; 65 66 private BubbleController mController; 67 private boolean mIsOverflow; 68 private BubbleTaskViewHelper mBubbleTaskViewHelper; 69 private BubbleBarMenuViewController mMenuViewController; 70 private @Nullable Supplier<Rect> mLayerBoundsSupplier; 71 private @Nullable Listener mListener; 72 73 private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext()); 74 private @Nullable TaskView mTaskView; 75 private @Nullable BubbleOverflowContainerView mOverflowView; 76 77 private int mCaptionHeight; 78 79 private int mBackgroundColor; 80 private float mCornerRadius = 0f; 81 82 /** 83 * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If 84 * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha 85 * value until the animation ends. 86 */ 87 private boolean mIsContentVisible = false; 88 private boolean mIsAnimating; 89 BubbleBarExpandedView(Context context)90 public BubbleBarExpandedView(Context context) { 91 this(context, null); 92 } 93 BubbleBarExpandedView(Context context, AttributeSet attrs)94 public BubbleBarExpandedView(Context context, AttributeSet attrs) { 95 this(context, attrs, 0); 96 } 97 BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr)98 public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { 99 this(context, attrs, defStyleAttr, 0); 100 } 101 BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)102 public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr, 103 int defStyleRes) { 104 super(context, attrs, defStyleAttr, defStyleRes); 105 } 106 107 @Override onFinishInflate()108 protected void onFinishInflate() { 109 super.onFinishInflate(); 110 Context context = getContext(); 111 setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation)); 112 mCaptionHeight = context.getResources().getDimensionPixelSize( 113 R.dimen.bubble_bar_expanded_view_caption_height); 114 addView(mHandleView); 115 applyThemeAttrs(); 116 setClipToOutline(true); 117 setOutlineProvider(new ViewOutlineProvider() { 118 @Override 119 public void getOutline(View view, Outline outline) { 120 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 121 } 122 }); 123 } 124 125 @Override onDetachedFromWindow()126 protected void onDetachedFromWindow() { 127 super.onDetachedFromWindow(); 128 // Hide manage menu when view disappears 129 mMenuViewController.hideMenu(false /* animated */); 130 } 131 132 /** Set the BubbleController on the view, must be called before doing anything else. */ initialize(BubbleController controller, boolean isOverflow)133 public void initialize(BubbleController controller, boolean isOverflow) { 134 mController = controller; 135 mIsOverflow = isOverflow; 136 137 if (mIsOverflow) { 138 mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate( 139 R.layout.bubble_overflow_container, null /* root */); 140 mOverflowView.setBubbleController(mController); 141 addView(mOverflowView); 142 } else { 143 144 mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController, 145 /* listener= */ this, 146 /* viewParent= */ this); 147 mTaskView = mBubbleTaskViewHelper.getTaskView(); 148 addView(mTaskView); 149 mTaskView.setEnableSurfaceClipping(true); 150 mTaskView.setCornerRadius(mCornerRadius); 151 152 // Handle view needs to draw on top of task view. 153 bringChildToFront(mHandleView); 154 } 155 mMenuViewController = new BubbleBarMenuViewController(mContext, this); 156 mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() { 157 @Override 158 public void onMenuVisibilityChanged(boolean visible) { 159 setObscured(visible); 160 } 161 162 @Override 163 public void onUnBubbleConversation(Bubble bubble) { 164 if (mListener != null) { 165 mListener.onUnBubbleConversation(bubble.getKey()); 166 } 167 } 168 169 @Override 170 public void onOpenAppSettings(Bubble bubble) { 171 mController.collapseStack(); 172 mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser()); 173 } 174 175 @Override 176 public void onDismissBubble(Bubble bubble) { 177 mController.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED); 178 } 179 }); 180 mHandleView.setOnClickListener(view -> { 181 mMenuViewController.showMenu(true /* animated */); 182 }); 183 } 184 getHandleView()185 public BubbleBarHandleView getHandleView() { 186 return mHandleView; 187 } 188 189 // TODO (b/275087636): call this when theme/config changes 190 /** Updates the view based on the current theme. */ applyThemeAttrs()191 public void applyThemeAttrs() { 192 boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows( 193 mContext.getResources()); 194 final TypedArray ta = mContext.obtainStyledAttributes(new int[]{ 195 android.R.attr.dialogCornerRadius, 196 android.R.attr.colorBackgroundFloating}); 197 mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0; 198 mCornerRadius = mCornerRadius / 2f; 199 mBackgroundColor = ta.getColor(1, Color.WHITE); 200 201 ta.recycle(); 202 203 mCaptionHeight = getResources().getDimensionPixelSize( 204 R.dimen.bubble_bar_expanded_view_caption_height); 205 206 if (mTaskView != null) { 207 mTaskView.setCornerRadius(mCornerRadius); 208 updateHandleColor(true /* animated */); 209 } 210 } 211 212 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)213 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 214 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 215 int height = MeasureSpec.getSize(heightMeasureSpec); 216 int menuViewHeight = Math.min(mCaptionHeight, height); 217 measureChild(mHandleView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight, 218 MeasureSpec.getMode(heightMeasureSpec))); 219 220 if (mTaskView != null) { 221 measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, 222 MeasureSpec.getMode(heightMeasureSpec))); 223 } 224 } 225 226 @Override onLayout(boolean changed, int l, int t, int r, int b)227 protected void onLayout(boolean changed, int l, int t, int r, int b) { 228 super.onLayout(changed, l, t, r, b); 229 final int captionBottom = t + mCaptionHeight; 230 if (mTaskView != null) { 231 mTaskView.layout(l, t, r, 232 t + mTaskView.getMeasuredHeight()); 233 mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0)); 234 } 235 // Handle draws on top of task view in the caption area. 236 mHandleView.layout(l, t, r, captionBottom); 237 } 238 239 @Override onTaskCreated()240 public void onTaskCreated() { 241 setContentVisibility(true); 242 updateHandleColor(false /* animated */); 243 if (mListener != null) { 244 mListener.onTaskCreated(); 245 } 246 } 247 248 @Override onContentVisibilityChanged(boolean visible)249 public void onContentVisibilityChanged(boolean visible) { 250 setContentVisibility(visible); 251 } 252 253 @Override onBackPressed()254 public void onBackPressed() { 255 if (mListener == null) return; 256 mListener.onBackPressed(); 257 } 258 259 /** Cleans up task view, should be called when the bubble is no longer active. */ cleanUpExpandedState()260 public void cleanUpExpandedState() { 261 if (mBubbleTaskViewHelper != null) { 262 if (mTaskView != null) { 263 removeView(mTaskView); 264 } 265 mBubbleTaskViewHelper.cleanUpTaskView(); 266 } 267 mMenuViewController.hideMenu(false /* animated */); 268 } 269 270 /** 271 * Hides the current modal menu view or collapses the bubble stack. 272 * Called from {@link BubbleBarLayerView} 273 */ hideMenuOrCollapse()274 public void hideMenuOrCollapse() { 275 if (mMenuViewController.isMenuVisible()) { 276 mMenuViewController.hideMenu(/* animated = */ true); 277 } else { 278 mController.collapseStack(); 279 } 280 } 281 282 /** Updates the bubble shown in the expanded view. */ update(Bubble bubble)283 public void update(Bubble bubble) { 284 mBubbleTaskViewHelper.update(bubble); 285 mMenuViewController.updateMenu(bubble); 286 } 287 288 /** The task id of the activity shown in the task view, if it exists. */ getTaskId()289 public int getTaskId() { 290 return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID; 291 } 292 293 /** Sets layer bounds supplier used for obscured touchable region of task view */ setLayerBoundsSupplier(@ullable Supplier<Rect> supplier)294 void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) { 295 mLayerBoundsSupplier = supplier; 296 } 297 298 /** Sets expanded view listener */ setListener(@ullable Listener listener)299 void setListener(@Nullable Listener listener) { 300 mListener = listener; 301 } 302 303 /** Sets whether the view is obscured by some modal view */ setObscured(boolean obscured)304 void setObscured(boolean obscured) { 305 if (mTaskView == null || mLayerBoundsSupplier == null) return; 306 // Updates the obscured touchable region for the task surface. 307 mTaskView.setObscuredTouchRect(obscured ? mLayerBoundsSupplier.get() : null); 308 } 309 310 /** 311 * Call when the location or size of the view has changed to update TaskView. 312 */ updateLocation()313 public void updateLocation() { 314 if (mTaskView != null) { 315 mTaskView.onLocationChanged(); 316 } 317 } 318 319 /** Shows the expanded view for the overflow if it exists. */ maybeShowOverflow()320 void maybeShowOverflow() { 321 if (mOverflowView != null) { 322 // post this to the looper so that the view has a chance to be laid out before it can 323 // calculate row and column sizes correctly. 324 post(() -> mOverflowView.show()); 325 } 326 } 327 328 /** Sets the alpha of the task view. */ setContentVisibility(boolean visible)329 public void setContentVisibility(boolean visible) { 330 mIsContentVisible = visible; 331 332 if (mTaskView == null) return; 333 334 if (!mIsAnimating) { 335 mTaskView.setAlpha(visible ? 1f : 0f); 336 } 337 } 338 339 /** 340 * Updates the handle color based on the task view status bar or background color; if those 341 * are transparent it defaults to the background color pulled from system theme attributes. 342 */ updateHandleColor(boolean animated)343 private void updateHandleColor(boolean animated) { 344 if (mTaskView == null || mTaskView.getTaskInfo() == null) return; 345 int color = mBackgroundColor; 346 ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription; 347 if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) { 348 color = taskDescription.getStatusBarColor(); 349 } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) { 350 color = taskDescription.getBackgroundColor(); 351 } 352 final boolean isRegionDark = Color.luminance(color) <= 0.5; 353 mHandleView.updateHandleColor(isRegionDark, animated); 354 } 355 356 /** 357 * Sets the alpha of both this view and the task view. 358 */ setTaskViewAlpha(float alpha)359 public void setTaskViewAlpha(float alpha) { 360 if (mTaskView != null) { 361 mTaskView.setAlpha(alpha); 362 } 363 setAlpha(alpha); 364 } 365 366 /** 367 * Sets whether the surface displaying app content should sit on top. This is useful for 368 * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble 369 * being dragged out, the manage menu) this is set to false, otherwise it should be true. 370 */ setSurfaceZOrderedOnTop(boolean onTop)371 public void setSurfaceZOrderedOnTop(boolean onTop) { 372 if (mTaskView == null) { 373 return; 374 } 375 mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */); 376 } 377 378 /** 379 * Sets whether the view is animating, in this case we won't change the content visibility 380 * until the animation is done. 381 */ setAnimating(boolean animating)382 public void setAnimating(boolean animating) { 383 mIsAnimating = animating; 384 // If we're done animating, apply the correct visibility. 385 if (!animating) { 386 setContentVisibility(mIsContentVisible); 387 } 388 } 389 } 390