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