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