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