• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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;
18 
19 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
20 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
21 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
22 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
23 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
24 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
25 
26 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
27 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
28 import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT;
29 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
30 import static com.android.wm.shell.shared.TypefaceUtils.setTypeface;
31 
32 import android.annotation.NonNull;
33 import android.annotation.SuppressLint;
34 import android.app.ActivityOptions;
35 import android.app.PendingIntent;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.res.Resources;
40 import android.content.res.TypedArray;
41 import android.graphics.Bitmap;
42 import android.graphics.CornerPathEffect;
43 import android.graphics.Outline;
44 import android.graphics.Paint;
45 import android.graphics.Picture;
46 import android.graphics.PointF;
47 import android.graphics.PorterDuff;
48 import android.graphics.Rect;
49 import android.graphics.drawable.ShapeDrawable;
50 import android.util.AttributeSet;
51 import android.util.FloatProperty;
52 import android.util.IntProperty;
53 import android.util.Log;
54 import android.util.TypedValue;
55 import android.view.ContextThemeWrapper;
56 import android.view.LayoutInflater;
57 import android.view.TouchDelegate;
58 import android.view.View;
59 import android.view.ViewGroup;
60 import android.view.ViewOutlineProvider;
61 import android.view.accessibility.AccessibilityNodeInfo;
62 import android.widget.FrameLayout;
63 import android.widget.LinearLayout;
64 import android.window.ScreenCapture;
65 
66 import androidx.annotation.Nullable;
67 
68 import com.android.internal.annotations.VisibleForTesting;
69 import com.android.internal.policy.ScreenDecorationsUtils;
70 import com.android.internal.protolog.ProtoLog;
71 import com.android.wm.shell.Flags;
72 import com.android.wm.shell.R;
73 import com.android.wm.shell.common.AlphaOptimizedButton;
74 import com.android.wm.shell.shared.TriangleShape;
75 import com.android.wm.shell.shared.TypefaceUtils;
76 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
77 import com.android.wm.shell.taskview.TaskView;
78 
79 import java.io.PrintWriter;
80 
81 /**
82  * Container for the expanded bubble view, handles rendering the caret and settings icon.
83  */
84 public class BubbleExpandedView extends LinearLayout {
85     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
86 
87     /** {@link IntProperty} for updating bottom clip */
88     public static final IntProperty<BubbleExpandedView> BOTTOM_CLIP_PROPERTY =
89             new IntProperty<BubbleExpandedView>("bottomClip") {
90                 @Override
91                 public void setValue(BubbleExpandedView expandedView, int value) {
92                     expandedView.setBottomClip(value);
93                 }
94 
95                 @Override
96                 public Integer get(BubbleExpandedView expandedView) {
97                     return expandedView.mBottomClip;
98                 }
99             };
100 
101     /** {@link FloatProperty} for updating taskView or overflow alpha */
102     public static final FloatProperty<BubbleExpandedView> CONTENT_ALPHA =
103             new FloatProperty<BubbleExpandedView>("contentAlpha") {
104                 @Override
105                 public void setValue(BubbleExpandedView expandedView, float value) {
106                     expandedView.setContentAlpha(value);
107                 }
108 
109                 @Override
110                 public Float get(BubbleExpandedView expandedView) {
111                     return expandedView.getContentAlpha();
112                 }
113             };
114 
115     /** {@link FloatProperty} for updating background and pointer alpha */
116     public static final FloatProperty<BubbleExpandedView> BACKGROUND_ALPHA =
117             new FloatProperty<BubbleExpandedView>("backgroundAlpha") {
118                 @Override
119                 public void setValue(BubbleExpandedView expandedView, float value) {
120                     expandedView.setBackgroundAlpha(value);
121                 }
122 
123                 @Override
124                 public Float get(BubbleExpandedView expandedView) {
125                     return expandedView.getAlpha();
126                 }
127             };
128 
129     /** {@link FloatProperty} for updating manage button alpha */
130     public static final FloatProperty<BubbleExpandedView> MANAGE_BUTTON_ALPHA =
131             new FloatProperty<BubbleExpandedView>("manageButtonAlpha") {
132                 @Override
133                 public void setValue(BubbleExpandedView expandedView, float value) {
134                     expandedView.mManageButton.setAlpha(value);
135                 }
136 
137                 @Override
138                 public Float get(BubbleExpandedView expandedView) {
139                     return expandedView.mManageButton.getAlpha();
140                 }
141             };
142 
143     // The triangle pointing to the expanded view
144     private View mPointerView;
145     @Nullable private int[] mExpandedViewContainerLocation;
146 
147     private AlphaOptimizedButton mManageButton;
148     private TaskView mTaskView;
149     private BubbleOverflowContainerView mOverflowView;
150 
151     private int mTaskId = INVALID_TASK_ID;
152 
153     private boolean mImeVisible;
154     private boolean mNeedsNewHeight;
155 
156     /**
157      * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
158      * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
159      * value until the animation ends.
160      */
161     private boolean mIsContentVisible = false;
162 
163     /**
164      * Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on
165      * applying alpha changes from {@link #setContentVisibility} until the animation ends.
166      */
167     private boolean mIsAnimating = false;
168 
169     private int mPointerWidth;
170     private int mPointerHeight;
171     private float mPointerRadius;
172     private float mPointerOverlap;
173     private final PointF mPointerPos = new PointF();
174     private CornerPathEffect mPointerEffect;
175     private ShapeDrawable mCurrentPointer;
176     private ShapeDrawable mTopPointer;
177     private ShapeDrawable mLeftPointer;
178     private ShapeDrawable mRightPointer;
179     private float mCornerRadius = 0f;
180     private int mBackgroundColorFloating;
181     private boolean mUsingMaxHeight;
182     private int mLeftClip = 0;
183     private int mTopClip = 0;
184     private int mRightClip = 0;
185     private int mBottomClip = 0;
186     @Nullable private Bubble mBubble;
187     private PendingIntent mPendingIntent;
188     // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead
189     private boolean mIsOverflow;
190     private boolean mIsClipping;
191 
192     private BubbleExpandedViewManager mManager;
193     private BubbleStackView mStackView;
194     private BubblePositioner mPositioner;
195 
196     /**
197      * Container for the {@code TaskView} that has a solid, round-rect background that shows if the
198      * {@code TaskView} hasn't loaded.
199      */
200     private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext());
201 
202     private TaskView.Listener mCurrentTaskViewListener;
203 
204     private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
205         private boolean mInitialized = false;
206         private boolean mDestroyed = false;
207 
208         @Override
209         public void onInitialized() {
210             if (mDestroyed || mInitialized) {
211                 ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s",
212                         mDestroyed, mInitialized, getBubbleKey());
213                 return;
214             }
215 
216             // Custom options so there is no activity transition animation
217             ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
218                     0 /* enterResId */, 0 /* exitResId */);
219 
220             // TODO: I notice inconsistencies in lifecycle
221             // Post to keep the lifecycle normal
222             post(() -> {
223                 ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s",
224                         getBubbleKey());
225                 try {
226                     Rect launchBounds = new Rect();
227                     mTaskView.getBoundsOnScreen(launchBounds);
228 
229                     options.setTaskAlwaysOnTop(true);
230                     options.setPendingIntentBackgroundActivityStartMode(
231                             MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
232 
233                     final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId()
234                             || (mBubble.isShortcut()
235                             && BubbleAnythingFlagHelper.enableCreateAnyBubble()));
236 
237                     // TODO - currently based on type, really it's what the "launch item" is.
238                     if (mBubble.isApp() || mBubble.isNote()) {
239                         Context context =
240                                 mContext.createContextAsUser(
241                                         mBubble.getUser(), Context.CONTEXT_RESTRICTED);
242                         Intent fillInIntent = new Intent();
243                         PendingIntent pi = PendingIntent.getActivity(
244                                 context,
245                                 /* requestCode= */ 0,
246                                 mBubble.getIntent(),
247                                 // Needs to be mutable for the fillInIntent
248                                 PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
249                                 /* options= */ null);
250                         mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
251                     } else if (!mIsOverflow && isShortcutBubble) {
252                         ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey());
253                         if (mBubble.isChat()) {
254                             options.setLaunchedFromBubble(true);
255                             options.setApplyActivityFlagsForBubbles(true);
256                         } else {
257                             options.setApplyMultipleTaskFlagForShortcut(true);
258                         }
259                         mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
260                                 options, launchBounds);
261                     } else {
262                         options.setLaunchedFromBubble(true);
263                         if (mBubble != null) {
264                             mBubble.setPendingIntentActive();
265                         }
266                         final Intent fillInIntent = new Intent();
267                         // Apply flags to make behaviour match documentLaunchMode=always.
268                         fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
269                         fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
270                         mTaskView.startActivity(mPendingIntent, fillInIntent, options,
271                                 launchBounds);
272                     }
273                 } catch (RuntimeException e) {
274                     // If there's a runtime exception here then there's something
275                     // wrong with the intent, we can't really recover / try to populate
276                     // the bubble again so we'll just remove it.
277                     Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
278                             + ", " + e.getMessage() + "; removing bubble");
279                     mManager.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
280                 }
281             });
282             mInitialized = true;
283         }
284 
285         @Override
286         public void onReleased() {
287             mDestroyed = true;
288         }
289 
290         @Override
291         public void onTaskCreated(int taskId, ComponentName name) {
292             ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s",
293                     taskId, getBubbleKey());
294             // The taskId is saved to use for removeTask, preventing appearance in recent tasks.
295             mTaskId = taskId;
296 
297             if (mBubble != null && mBubble.isNote()) {
298                 // Let the controller know sooner what the taskId is.
299                 mManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId);
300             }
301 
302             // With the task org, the taskAppeared callback will only happen once the task has
303             // already drawn
304             setContentVisibility(true);
305         }
306 
307         @Override
308         public void onTaskVisibilityChanged(int taskId, boolean visible) {
309             ProtoLog.d(WM_SHELL_BUBBLES, "onTaskVisibilityChanged=%b bubble=%s taskId=%d",
310                     visible, getBubbleKey(), taskId);
311             setContentVisibility(visible);
312         }
313 
314         @Override
315         public void onTaskRemovalStarted(int taskId) {
316             ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s",
317                     taskId, getBubbleKey());
318             if (mBubble != null) {
319                 mManager.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED);
320             }
321             if (mTaskView != null) {
322                 // Release the surface
323                 mTaskView.release();
324                 removeView(mTaskView);
325                 mTaskView = null;
326             }
327         }
328 
329         @Override
330         public void onBackPressedOnTaskRoot(int taskId) {
331             if (mTaskId == taskId && mStackView.isExpanded()) {
332                 mStackView.onBackPressed();
333             }
334         }
335     };
336 
BubbleExpandedView(Context context)337     public BubbleExpandedView(Context context) {
338         this(context, null);
339     }
340 
BubbleExpandedView(Context context, AttributeSet attrs)341     public BubbleExpandedView(Context context, AttributeSet attrs) {
342         this(context, attrs, 0);
343     }
344 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)345     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
346         this(context, attrs, defStyleAttr, 0);
347     }
348 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)349     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
350             int defStyleRes) {
351         super(context, attrs, defStyleAttr, defStyleRes);
352     }
353 
354     @SuppressLint("ClickableViewAccessibility")
355     @Override
onFinishInflate()356     protected void onFinishInflate() {
357         super.onFinishInflate();
358         mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate(
359                 R.layout.bubble_manage_button, this /* parent */, false /* attach */);
360         updateDimensions();
361         mPointerView = findViewById(R.id.pointer_view);
362         mCurrentPointer = mTopPointer;
363         mPointerView.setVisibility(INVISIBLE);
364 
365         // Set {@code TaskView}'s alpha value as zero, since there is no view content to be shown.
366         setContentVisibility(false);
367 
368         mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() {
369             @Override
370             public void getOutline(View view, Outline outline) {
371                 Rect clip = new Rect(mLeftClip, mTopClip, view.getWidth() - mRightClip,
372                         view.getHeight() - mBottomClip);
373                 outline.setRoundRect(clip, mCornerRadius);
374             }
375         });
376         mExpandedViewContainer.setClipToOutline(true);
377         mExpandedViewContainer.setLayoutParams(
378                 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
379         addView(mExpandedViewContainer);
380 
381         // Expanded stack layout, top to bottom:
382         // Expanded view container
383         // ==> bubble row
384         // ==> expanded view
385         //   ==> activity view
386         //   ==> manage button
387         bringChildToFront(mManageButton);
388         setManageClickListener();
389 
390         applyThemeAttrs();
391 
392         setClipToPadding(false);
393         setOnTouchListener((view, motionEvent) -> {
394             if (mTaskView == null) {
395                 return false;
396             }
397 
398             final Rect avBounds = new Rect();
399             mTaskView.getBoundsOnScreen(avBounds);
400 
401             // Consume and ignore events on the expanded view padding that are within the
402             // {@code TaskView}'s vertical bounds. These events are part of a back gesture, and so
403             // they should not collapse the stack (which all other touches on areas around the AV
404             // would do).
405             if (motionEvent.getRawY() >= avBounds.top
406                     && motionEvent.getRawY() <= avBounds.bottom
407                     && (motionEvent.getRawX() < avBounds.left
408                     || motionEvent.getRawX() > avBounds.right)) {
409                 return true;
410             }
411 
412             return false;
413         });
414 
415         // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout
416         // so the Manage button appears on the right.
417         setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
418     }
419 
420 
421     /** Updates the width of the task view if it changed. */
updateTaskViewContentWidth()422     void updateTaskViewContentWidth() {
423         if (mTaskView != null) {
424             int width = getContentWidth();
425             if (mTaskView.getWidth() != width) {
426                 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(width, MATCH_PARENT);
427                 mTaskView.setLayoutParams(lp);
428             }
429         }
430     }
431 
getContentWidth()432     private int getContentWidth() {
433         boolean isStackOnLeft = mPositioner.isStackOnLeft(mStackView.getStackPosition());
434         return mPositioner.getTaskViewContentWidth(isStackOnLeft);
435     }
436 
437     /**
438      * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need
439      * to be called after view inflate.
440      */
initialize(BubbleExpandedViewManager expandedViewManager, BubbleStackView stackView, BubblePositioner positioner, boolean isOverflow, @Nullable BubbleTaskView bubbleTaskView)441     void initialize(BubbleExpandedViewManager expandedViewManager,
442             BubbleStackView stackView,
443             BubblePositioner positioner,
444             boolean isOverflow,
445             @Nullable BubbleTaskView bubbleTaskView) {
446         mManager = expandedViewManager;
447         mStackView = stackView;
448         mIsOverflow = isOverflow;
449         mPositioner = positioner;
450 
451         if (mIsOverflow) {
452             mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
453                     R.layout.bubble_overflow_container, null /* root */);
454             mOverflowView.initialize(expandedViewManager, positioner);
455             FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
456             mExpandedViewContainer.addView(mOverflowView, lp);
457             mExpandedViewContainer.setLayoutParams(
458                     new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
459             bringChildToFront(mOverflowView);
460             mManageButton.setVisibility(GONE);
461         } else {
462             mTaskView = bubbleTaskView.getTaskView();
463             // reset the insets that might left after TaskView is shown in BubbleBarExpandedView
464             mTaskView.setCaptionInsets(null);
465             if (Flags.enableBubbleTaskViewListener()) {
466                 mCurrentTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView,
467                         /* viewParent= */ this, expandedViewManager,
468                         new BubbleTaskViewListener.Callback() {
469                             @Override
470                             public void onTaskCreated() {
471                                 // The taskId is saved to use for removeTask,
472                                 // preventing appearance in recent tasks.
473                                 BubbleTaskViewListener listener = mCurrentTaskViewListener != null
474                                         ? ((BubbleTaskViewListener) mCurrentTaskViewListener)
475                                         : null;
476                                 mTaskId = listener != null
477                                         ? listener.getTaskId()
478                                         : bubbleTaskView.getTaskId();
479                                 setContentVisibility(true);
480                             }
481 
482                             @Override
483                             public void onContentVisibilityChanged(boolean visible) {
484                                 setContentVisibility(visible);
485                             }
486 
487                             @Override
488                             public void onBackPressed() {
489                                 mStackView.onBackPressed();
490                             }
491 
492                             @Override
493                             public void onTaskRemovalStarted() {
494                                 // nothing to do / handled in listener.
495                             }
496                         });
497             } else {
498                 mCurrentTaskViewListener = mTaskViewListener;
499                 bubbleTaskView.setDelegateListener(mCurrentTaskViewListener);
500             }
501 
502             // set a fixed width so it is not recalculated as part of a rotation. the width will be
503             // updated manually after the rotation.
504             FrameLayout.LayoutParams lp =
505                     new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT);
506             if (mTaskView.getParent() != null) {
507                 ((ViewGroup) mTaskView.getParent()).removeView(mTaskView);
508             }
509             mExpandedViewContainer.addView(mTaskView, lp);
510             bringChildToFront(mTaskView);
511 
512             if (!Flags.enableBubbleTaskViewListener()) {
513                 if (bubbleTaskView.isCreated()) {
514                     mCurrentTaskViewListener.onTaskCreated(
515                             bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName());
516                 }
517             }
518         }
519     }
520 
updateDimensions()521     void updateDimensions() {
522         Resources res = getResources();
523         updateFontSize();
524 
525         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
526         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
527         mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius);
528         mPointerEffect = new CornerPathEffect(mPointerRadius);
529         mPointerOverlap = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
530         mTopPointer = new ShapeDrawable(TriangleShape.create(
531                 mPointerWidth, mPointerHeight, true /* pointUp */));
532         mLeftPointer = new ShapeDrawable(TriangleShape.createHorizontal(
533                 mPointerWidth, mPointerHeight, true /* pointLeft */));
534         mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal(
535                 mPointerWidth, mPointerHeight, false /* pointLeft */));
536         updatePointerViewIfExists();
537         updateManageButtonIfExists();
538     }
539 
540 
541     /**
542      * Reinflate manage button if {@link #mManageButton} is initialized.
543      * Does nothing otherwise.
544      */
updateManageButtonIfExists()545     private void updateManageButtonIfExists() {
546         if (mManageButton == null) {
547             return;
548         }
549         int visibility = mManageButton.getVisibility();
550         removeView(mManageButton);
551         ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(),
552                 com.android.internal.R.style.Theme_DeviceDefault_DayNight);
553         mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate(
554                 R.layout.bubble_manage_button, this /* parent */, false /* attach */);
555         addView(mManageButton);
556         setTypeface(mManageButton, TypefaceUtils.FontFamily.GSF_LABEL_LARGE);
557         mManageButton.setVisibility(visibility);
558         setManageClickListener();
559         post(() -> {
560             int touchAreaHeight =
561                     getResources().getDimensionPixelSize(
562                             R.dimen.bubble_manage_button_touch_area_height);
563             Rect r = new Rect();
564             mManageButton.getHitRect(r);
565             int extraTouchArea = (touchAreaHeight - r.height()) / 2;
566             r.top -= extraTouchArea;
567             r.bottom += extraTouchArea;
568             setTouchDelegate(new TouchDelegate(r, mManageButton));
569         });
570     }
571 
updateFontSize()572     void updateFontSize() {
573         final float fontSize = mContext.getResources()
574                 .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material);
575         if (mManageButton != null) {
576             mManageButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
577         }
578         if (mOverflowView != null) {
579             mOverflowView.updateFontSize();
580         }
581     }
582 
updateLocale()583     void updateLocale() {
584         if (mManageButton != null) {
585             mManageButton.setText(mContext.getString(R.string.manage_bubbles_text));
586         }
587         if (mOverflowView != null) {
588             mOverflowView.updateLocale();
589         }
590     }
591 
applyThemeAttrs()592     void applyThemeAttrs() {
593         final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
594                 android.R.attr.dialogCornerRadius});
595         boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
596                 mContext.getResources());
597         mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
598         mBackgroundColorFloating = mContext.getColor(
599                 com.android.internal.R.color.materialColorSurfaceBright);
600         mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating);
601         final int manageMenuBg = mContext.getColor(
602                 com.android.internal.R.color.materialColorSurfaceContainerHigh);
603         ta.recycle();
604         if (mManageButton != null) {
605             mManageButton.getBackground().setColorFilter(manageMenuBg, PorterDuff.Mode.SRC_IN);
606         }
607 
608         if (mTaskView != null) {
609             mTaskView.setCornerRadius(mCornerRadius);
610         }
611         updatePointerViewIfExists();
612         updateManageButtonIfExists();
613     }
614 
getCornerRadius()615     public float getCornerRadius() {
616         return mCornerRadius;
617     }
618 
619     /**
620      * Updates the size and visuals of the pointer if {@link #mPointerView} is initialized.
621      * Does nothing otherwise.
622      */
updatePointerViewIfExists()623     private void updatePointerViewIfExists() {
624         if (mPointerView == null) {
625             return;
626         }
627         LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams();
628         if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) {
629             lp.width = mPointerHeight;
630             lp.height = mPointerWidth;
631         } else {
632             lp.width = mPointerWidth;
633             lp.height = mPointerHeight;
634         }
635         mCurrentPointer.setTint(mBackgroundColorFloating);
636 
637         Paint arrowPaint = mCurrentPointer.getPaint();
638         arrowPaint.setColor(mBackgroundColorFloating);
639         arrowPaint.setPathEffect(mPointerEffect);
640         mPointerView.setLayoutParams(lp);
641         mPointerView.setBackground(mCurrentPointer);
642     }
643 
644     @VisibleForTesting
getBubbleKey()645     public String getBubbleKey() {
646         return mBubble != null ? mBubble.getKey() : mIsOverflow ? BubbleOverflow.KEY : null;
647     }
648 
649     /**
650      * Sets whether the surface displaying app content should sit on top. This is useful for
651      * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
652      * being dragged out, the manage menu) this is set to false, otherwise it should be true.
653      */
setSurfaceZOrderedOnTop(boolean onTop)654     public void setSurfaceZOrderedOnTop(boolean onTop) {
655         if (mTaskView == null) {
656             return;
657         }
658         mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
659     }
660 
setImeVisible(boolean visible)661     void setImeVisible(boolean visible) {
662         mImeVisible = visible;
663         if (!mImeVisible && mNeedsNewHeight) {
664             updateHeight();
665         }
666     }
667 
668     /** Return a GraphicBuffer with the contents of the task view surface. */
669     @Nullable
snapshotActivitySurface()670     ScreenCapture.ScreenshotHardwareBuffer snapshotActivitySurface() {
671         if (mIsOverflow) {
672             // For now, just snapshot the view and return it as a hw buffer so that the animation
673             // code for both the tasks and overflow can be the same
674             Picture p = new Picture();
675             mOverflowView.draw(
676                     p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight()));
677             p.endRecording();
678             Bitmap snapshot = Bitmap.createBitmap(p);
679             return new ScreenCapture.ScreenshotHardwareBuffer(
680                     snapshot.getHardwareBuffer(),
681                     snapshot.getColorSpace(),
682                     false /* containsSecureLayers */,
683                     false /* containsHdrLayers */);
684         }
685         if (mTaskView == null || mTaskView.getSurfaceControl() == null) {
686             return null;
687         }
688         return ScreenCapture.captureLayers(
689                 mTaskView.getSurfaceControl(),
690                 new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()),
691                 1 /* scale */);
692     }
693 
getTaskViewLocationOnScreen()694     int[] getTaskViewLocationOnScreen() {
695         if (mIsOverflow) {
696             // This is only used for animating away the surface when switching bubbles, just use the
697             // view location on screen for now to allow us to use the same animation code with tasks
698             return mOverflowView.getLocationOnScreen();
699         }
700         if (mTaskView != null) {
701             return mTaskView.getLocationOnScreen();
702         } else {
703             return new int[]{0, 0};
704         }
705     }
706 
setManageClickListener()707     private void setManageClickListener() {
708         mManageButton.setOnClickListener(v -> mStackView.onManageBubbleClicked());
709     }
710 
711     /**
712      * Updates the obscured touchable region for the task surface. This calls onLocationChanged,
713      * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is
714      * useful if a view has been added or removed from on top of the {@code TaskView}, such as the
715      * manage menu.
716      */
updateObscuredTouchableRegion()717     void updateObscuredTouchableRegion() {
718         if (mTaskView != null) {
719             mTaskView.onLocationChanged();
720         }
721     }
722 
723     @Override
onDetachedFromWindow()724     protected void onDetachedFromWindow() {
725         super.onDetachedFromWindow();
726         mImeVisible = false;
727         mNeedsNewHeight = false;
728     }
729 
730     /**
731      * Whether we are currently animating the {@code TaskView}. If this is set to
732      * true, calls to {@link #setContentVisibility} will not be applied until this is set to false
733      * again.
734      */
setAnimating(boolean animating)735     public void setAnimating(boolean animating) {
736         mIsAnimating = animating;
737 
738         // If we're done animating, apply the correct
739         if (!animating) {
740             setContentVisibility(mIsContentVisible);
741         }
742     }
743 
744     /**
745      * Get alpha from underlying {@code TaskView} if this view is for a bubble.
746      * Or get alpha for the overflow view if this view is for overflow.
747      *
748      * @return alpha for the content being shown
749      */
getContentAlpha()750     public float getContentAlpha() {
751         if (mIsOverflow) {
752             return mOverflowView.getAlpha();
753         }
754         if (mTaskView != null) {
755             return mTaskView.getAlpha();
756         }
757         return 1f;
758     }
759 
760     /**
761      * Set alpha of the underlying {@code TaskView} if this view is for a bubble.
762      * Or set alpha for the overflow view if this view is for overflow.
763      *
764      * Changing expanded view's alpha does not affect the {@code TaskView} since it uses a Surface.
765      */
setContentAlpha(float alpha)766     public void setContentAlpha(float alpha) {
767         if (mIsOverflow) {
768             mOverflowView.setAlpha(alpha);
769         } else if (mTaskView != null) {
770             mTaskView.setAlpha(alpha);
771         }
772     }
773 
774     /** Sets the alpha of the background. */
setBackgroundAlpha(float alpha)775     public void setBackgroundAlpha(float alpha) {
776         if (Flags.enableNewBubbleAnimations()) {
777             setAlpha(alpha);
778         } else {
779             mPointerView.setAlpha(alpha);
780             setAlpha(alpha);
781         }
782     }
783 
784     /**
785      * Set translation Y for the expanded view content.
786      * Excludes manage button and pointer.
787      */
setContentTranslationY(float translationY)788     public void setContentTranslationY(float translationY) {
789         mExpandedViewContainer.setTranslationY(translationY);
790 
791         // Left or right pointer can become detached when moving the view up
792         if (translationY <= 0 && (isShowingLeftPointer() || isShowingRightPointer())) {
793             // Y coordinate where the pointer would start to get detached from the expanded view.
794             // Takes into account bottom clipping and rounded corners
795             float detachPoint =
796                     mExpandedViewContainer.getBottom() - mBottomClip - mCornerRadius + translationY;
797             float pointerBottom = mPointerPos.y + mPointerHeight;
798             // If pointer bottom is past detach point, move it in by that many pixels
799             float horizontalShift = 0;
800             if (pointerBottom > detachPoint) {
801                 horizontalShift = pointerBottom - detachPoint;
802             }
803             if (isShowingLeftPointer()) {
804                 // Move left pointer right
805                 movePointerBy(horizontalShift, 0);
806             } else {
807                 // Move right pointer left
808                 movePointerBy(-horizontalShift, 0);
809             }
810             // Hide pointer if it is moved by entire width
811             mPointerView.setVisibility(
812                     horizontalShift > mPointerWidth ? View.INVISIBLE : View.VISIBLE);
813         }
814     }
815 
816     /**
817      * Update alpha value for the manage button
818      */
setManageButtonAlpha(float alpha)819     public void setManageButtonAlpha(float alpha) {
820         mManageButton.setAlpha(alpha);
821     }
822 
823     /**
824      * Set {@link #setTranslationY(float) translationY} for the manage button
825      */
setManageButtonTranslationY(float translationY)826     public void setManageButtonTranslationY(float translationY) {
827         mManageButton.setTranslationY(translationY);
828     }
829 
830     /**
831      * Set top clipping for the view
832      */
setTopClip(int clip)833     public void setTopClip(int clip) {
834         mTopClip = clip;
835         onContainerClipUpdate();
836     }
837 
838     /**
839      * Set bottom clipping for the view
840      */
setBottomClip(int clip)841     public void setBottomClip(int clip) {
842         mBottomClip = clip;
843         onContainerClipUpdate();
844     }
845 
846     /** Updates the clip bounds. */
onContainerClipUpdate()847     public void onContainerClipUpdate() {
848         if (mTopClip == 0 && mBottomClip == 0 && mRightClip == 0 && mLeftClip == 0) {
849             if (mIsClipping) {
850                 mIsClipping = false;
851                 if (mTaskView != null) {
852                     mTaskView.setClipBounds(null);
853                     mTaskView.setEnableSurfaceClipping(false);
854                 }
855                 mExpandedViewContainer.invalidateOutline();
856             }
857         } else {
858             if (!mIsClipping) {
859                 mIsClipping = true;
860                 if (mTaskView != null) {
861                     mTaskView.setEnableSurfaceClipping(true);
862                 }
863             }
864             mExpandedViewContainer.invalidateOutline();
865             if (mTaskView != null) {
866                 Rect clipBounds = new Rect(mLeftClip, mTopClip,
867                         mTaskView.getWidth() - mRightClip,
868                         mTaskView.getHeight() - mBottomClip);
869                 mTaskView.setClipBounds(clipBounds);
870             }
871         }
872     }
873 
874     /**
875      * Move pointer from base position
876      */
movePointerBy(float x, float y)877     public void movePointerBy(float x, float y) {
878         mPointerView.setTranslationX(mPointerPos.x + x);
879         mPointerView.setTranslationY(mPointerPos.y + y);
880     }
881 
882     /**
883      * Set visibility of contents in the expanded state.
884      *
885      * @param visibility {@code true} if the contents should be visible on the screen.
886      *
887      * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
888      * and setting {@code false} actually means rendering the contents in transparent.
889      */
setContentVisibility(boolean visibility)890     public void setContentVisibility(boolean visibility) {
891         mIsContentVisible = visibility;
892         if (mTaskView != null && !mIsAnimating) {
893             mTaskView.setAlpha(visibility ? 1f : 0f);
894             mPointerView.setAlpha(visibility ? 1f : 0f);
895         }
896     }
897 
898     @Nullable
getTaskView()899     TaskView getTaskView() {
900         return mTaskView;
901     }
902 
903     @VisibleForTesting
getOverflow()904     public BubbleOverflowContainerView getOverflow() {
905         return mOverflowView;
906     }
907 
908 
909     /**
910      * Return content height: taskView or overflow.
911      * Takes into account clippings set by {@link #setTopClip(int)} and {@link #setBottomClip(int)}
912      *
913      * @return if bubble is for overflow, return overflow height, otherwise return taskView height
914      */
getContentHeight()915     public int getContentHeight() {
916         if (mIsOverflow) {
917             return mOverflowView.getHeight() - mTopClip - mBottomClip;
918         }
919         if (mTaskView != null) {
920             return mTaskView.getHeight() - mTopClip - mBottomClip;
921         }
922         return 0;
923     }
924 
925     /**
926      * Return bottom position of the content on screen
927      *
928      * @return if bubble is for overflow, return value for overflow, otherwise taskView
929      */
getContentBottomOnScreen()930     public int getContentBottomOnScreen() {
931         Rect out = new Rect();
932         if (mIsOverflow) {
933             mOverflowView.getBoundsOnScreen(out);
934         }
935         if (mTaskView != null) {
936             mTaskView.getBoundsOnScreen(out);
937         }
938         return out.bottom;
939     }
940 
getTaskId()941     int getTaskId() {
942         return mTaskId;
943     }
944 
945     /**
946      * Sets the bubble used to populate this view.
947      */
update(Bubble bubble)948     void update(Bubble bubble) {
949         if (mStackView == null) {
950             Log.w(TAG, "Stack is null for bubble: " + bubble);
951             return;
952         }
953         boolean isNew;
954         if (mCurrentTaskViewListener instanceof BubbleTaskViewListener) {
955             isNew = ((BubbleTaskViewListener) mCurrentTaskViewListener).setBubble(bubble);
956         } else {
957             isNew = mBubble == null || didBackingContentChange(bubble);
958         }
959         boolean isUpdate = bubble != null && mBubble != null
960                 && bubble.getKey().equals(mBubble.getKey());
961         ProtoLog.d(WM_SHELL_BUBBLES, "BubbleExpandedView - update bubble=%s; isNew=%b; isUpdate=%b",
962                 bubble.getKey(), isNew, isUpdate);
963         if (isNew || isUpdate) {
964             mBubble = bubble;
965             mManageButton.setContentDescription(getResources().getString(
966                     R.string.bubbles_settings_button_description, bubble.getAppName()));
967             mManageButton.setAccessibilityDelegate(
968                     new AccessibilityDelegate() {
969                         @Override
970                         public void onInitializeAccessibilityNodeInfo(View host,
971                                 AccessibilityNodeInfo info) {
972                             super.onInitializeAccessibilityNodeInfo(host, info);
973                             // On focus, have TalkBack say
974                             // "Actions available. Use swipe up then right to view."
975                             // in addition to the default "double tap to activate".
976                             mStackView.setupLocalMenu(info);
977                         }
978                     });
979 
980             if (isNew) {
981                 mPendingIntent = mBubble.getPendingIntent();
982                 if ((mPendingIntent != null || mBubble.hasMetadataShortcutId())
983                         && mTaskView != null) {
984                     setContentVisibility(false);
985                     mTaskView.setVisibility(VISIBLE);
986                 }
987             }
988             applyThemeAttrs();
989         } else {
990             Log.w(TAG, "Trying to update entry with different key, new bubble: "
991                     + bubble.getKey() + " old bubble: " + bubble.getKey());
992         }
993     }
994 
995     /**
996      * Bubbles are backed by a pending intent or a shortcut, once the activity is
997      * started we never change it / restart it on notification updates -- unless the bubbles'
998      * backing data switches.
999      *
1000      * This indicates if the new bubble is backed by a different data source than what was
1001      * previously shown here (e.g. previously a pending intent & now a shortcut).
1002      *
1003      * @param newBubble the bubble this view is being updated with.
1004      * @return true if the backing content has changed.
1005      */
didBackingContentChange(Bubble newBubble)1006     private boolean didBackingContentChange(Bubble newBubble) {
1007         boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
1008         boolean newIsIntentBased = newBubble.getPendingIntent() != null;
1009         return prevWasIntentBased != newIsIntentBased;
1010     }
1011 
1012     /**
1013      * Whether the bubble is using all available height to display or not.
1014      */
isUsingMaxHeight()1015     public boolean isUsingMaxHeight() {
1016         return mUsingMaxHeight;
1017     }
1018 
updateHeight()1019     void updateHeight() {
1020         if (mExpandedViewContainerLocation == null) {
1021             return;
1022         }
1023 
1024         if ((mBubble != null && mTaskView != null) || mIsOverflow) {
1025             float desiredHeight = mPositioner.getExpandedViewHeight(mBubble);
1026             int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow);
1027             float height = desiredHeight == MAX_HEIGHT
1028                     ? maxHeight
1029                     : Math.min(desiredHeight, maxHeight);
1030             mUsingMaxHeight = height == maxHeight;
1031             FrameLayout.LayoutParams lp = mIsOverflow
1032                     ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams()
1033                     : (FrameLayout.LayoutParams) mTaskView.getLayoutParams();
1034             mNeedsNewHeight = lp.height != height;
1035             if (!mImeVisible) {
1036                 // If the ime is visible... don't adjust the height because that will cause
1037                 // a configuration change and the ime will be lost.
1038                 lp.height = (int) height;
1039                 if (mIsOverflow) {
1040                     mOverflowView.setLayoutParams(lp);
1041                 } else {
1042                     mTaskView.setLayoutParams(lp);
1043                 }
1044                 mNeedsNewHeight = false;
1045             }
1046         }
1047     }
1048 
1049     /**
1050      * Update appearance of the expanded view being displayed.
1051      *
1052      * @param containerLocationOnScreen The location on-screen of the container the expanded view is
1053      *                                  added to. This allows us to calculate max height without
1054      *                                  waiting for layout.
1055      */
updateView(int[] containerLocationOnScreen)1056     public void updateView(int[] containerLocationOnScreen) {
1057         mExpandedViewContainerLocation = containerLocationOnScreen;
1058         updateHeight();
1059         if (mTaskView != null
1060                 && mTaskView.getVisibility() == VISIBLE
1061                 && mTaskView.isAttachedToWindow()) {
1062             // post this to the looper, because if the device orientation just changed, we need to
1063             // let the current shell transition complete before updating the task view bounds.
1064             post(() -> {
1065                 if (mTaskView != null) {
1066                     mTaskView.onLocationChanged();
1067                 }
1068             });
1069         }
1070         if (mIsOverflow) {
1071             // post this to the looper so that the view has a chance to be laid out before it can
1072             // calculate row and column sizes correctly.
1073             post(() -> mOverflowView.show());
1074         }
1075     }
1076 
1077     /**
1078      * Sets the position of the pointer.
1079      *
1080      * When bubbles are showing "vertically" they display along the left / right sides of the
1081      * screen with the expanded view beside them.
1082      *
1083      * If they aren't showing vertically they're positioned along the top of the screen with the
1084      * expanded view below them.
1085      *
1086      * @param bubblePosition the x position of the bubble if showing on top, the y position of
1087      *                       the bubble if showing vertically.
1088      * @param onLeft whether the stack was on the left side of the screen when expanded.
1089      * @param animate whether the pointer should animate to this position.
1090      */
setPointerPosition(float bubblePosition, boolean onLeft, boolean animate)1091     public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) {
1092         final boolean isRtl = mContext.getResources().getConfiguration().getLayoutDirection()
1093                 == LAYOUT_DIRECTION_RTL;
1094         // Pointer gets drawn in the padding
1095         final boolean showVertically = mPositioner.showBubblesVertically();
1096         final float paddingLeft = (showVertically && onLeft)
1097                 ? mPointerHeight - mPointerOverlap
1098                 : 0;
1099         final float paddingRight = (showVertically && !onLeft)
1100                 ? mPointerHeight - mPointerOverlap
1101                 : 0;
1102         final float paddingTop = showVertically
1103                 ? 0
1104                 : mPointerHeight - mPointerOverlap;
1105         setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0);
1106 
1107         // Subtract the expandedViewY here because the pointer is placed within the expandedView.
1108         float pointerPosition = mPositioner.getPointerPosition(bubblePosition);
1109         final float bubbleCenter = mPositioner.showBubblesVertically()
1110                 ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition)
1111                 : pointerPosition;
1112         // Post because we need the width of the view
1113         post(() -> {
1114             mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
1115             updatePointerViewIfExists();
1116             if (showVertically) {
1117                 mPointerPos.y = bubbleCenter - (mPointerWidth / 2f);
1118                 if (!isRtl) {
1119                     mPointerPos.x = onLeft
1120                             ? -mPointerHeight + mPointerOverlap
1121                             : getWidth() - mPaddingRight - mPointerOverlap;
1122                 } else {
1123                     mPointerPos.x = onLeft
1124                             ? -(getWidth() - mPaddingLeft - mPointerOverlap)
1125                             : mPointerHeight - mPointerOverlap;
1126                 }
1127             } else {
1128                 mPointerPos.y = mPointerOverlap;
1129                 if (!isRtl) {
1130                     mPointerPos.x = bubbleCenter - (mPointerWidth / 2f);
1131                 } else {
1132                     mPointerPos.x = -(getWidth() - mPaddingLeft - bubbleCenter)
1133                             + (mPointerWidth / 2f);
1134                 }
1135             }
1136             if (animate) {
1137                 mPointerView.animate().translationX(mPointerPos.x).translationY(
1138                         mPointerPos.y).start();
1139             } else {
1140                 mPointerView.setTranslationY(mPointerPos.y);
1141                 mPointerView.setTranslationX(mPointerPos.x);
1142                 mPointerView.setVisibility(VISIBLE);
1143             }
1144         });
1145     }
1146 
1147     /**
1148      * Return true if pointer is shown on the left
1149      */
isShowingLeftPointer()1150     public boolean isShowingLeftPointer() {
1151         return mCurrentPointer == mLeftPointer;
1152     }
1153 
1154     /**
1155      * Return true if pointer is shown on the right
1156      */
isShowingRightPointer()1157     public boolean isShowingRightPointer() {
1158         return mCurrentPointer == mRightPointer;
1159     }
1160 
1161     /**
1162      * Position of the manage button displayed in the expanded view. Used for placing user
1163      * education about the manage button.
1164      */
getManageButtonBoundsOnScreen(Rect rect)1165     public void getManageButtonBoundsOnScreen(Rect rect) {
1166         mManageButton.getBoundsOnScreen(rect);
1167     }
1168 
getManageButtonMargin()1169     public int getManageButtonMargin() {
1170         return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
1171     }
1172 
1173     /** Hide the task view. */
cleanUpExpandedState()1174     public void cleanUpExpandedState() {
1175         if (mTaskView != null) {
1176             mTaskView.setVisibility(GONE);
1177         }
1178     }
1179 
1180     /**
1181      * Description of current expanded view state.
1182      */
dump(@onNull PrintWriter pw, @NonNull String prefix)1183     public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
1184         pw.print(prefix); pw.println("BubbleExpandedView:");
1185         pw.print(prefix); pw.print("  taskId: "); pw.println(mTaskId);
1186         pw.print(prefix); pw.print("  stackView: "); pw.println(mStackView);
1187         pw.print(prefix); pw.print("  contentVisibility: "); pw.println(mIsContentVisible);
1188         pw.print(prefix); pw.print("  isAnimating: "); pw.println(mIsAnimating);
1189     }
1190 }
1191