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.Rect;
47 import android.graphics.drawable.ShapeDrawable;
48 import android.os.RemoteException;
49 import android.util.AttributeSet;
50 import android.util.Log;
51 import android.util.TypedValue;
52 import android.view.LayoutInflater;
53 import android.view.SurfaceControl;
54 import android.view.View;
55 import android.view.ViewGroup;
56 import android.view.ViewOutlineProvider;
57 import android.view.accessibility.AccessibilityNodeInfo;
58 import android.widget.FrameLayout;
59 import android.widget.LinearLayout;
60 
61 import androidx.annotation.Nullable;
62 
63 import com.android.internal.policy.ScreenDecorationsUtils;
64 import com.android.wm.shell.R;
65 import com.android.wm.shell.TaskView;
66 import com.android.wm.shell.common.AlphaOptimizedButton;
67 import com.android.wm.shell.common.TriangleShape;
68 
69 import java.io.FileDescriptor;
70 import java.io.PrintWriter;
71 
72 /**
73  * Container for the expanded bubble view, handles rendering the caret and settings icon.
74  */
75 public class BubbleExpandedView extends LinearLayout {
76     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES;
77 
78     // The triangle pointing to the expanded view
79     private View mPointerView;
80     @Nullable private int[] mExpandedViewContainerLocation;
81 
82     private AlphaOptimizedButton mManageButton;
83     private TaskView mTaskView;
84     private BubbleOverflowContainerView mOverflowView;
85 
86     private int mTaskId = INVALID_TASK_ID;
87 
88     private boolean mImeVisible;
89     private boolean mNeedsNewHeight;
90 
91     /**
92      * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
93      * {@link #mIsAlphaAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
94      * value until the animation ends.
95      */
96     private boolean mIsContentVisible = false;
97 
98     /**
99      * Whether we're animating the {@code TaskView}'s alpha value. If so, we will hold off on
100      * applying alpha changes from {@link #setContentVisibility} until the animation ends.
101      */
102     private boolean mIsAlphaAnimating = false;
103 
104     private int mPointerWidth;
105     private int mPointerHeight;
106     private float mPointerRadius;
107     private float mPointerOverlap;
108     private CornerPathEffect mPointerEffect;
109     private ShapeDrawable mCurrentPointer;
110     private ShapeDrawable mTopPointer;
111     private ShapeDrawable mLeftPointer;
112     private ShapeDrawable mRightPointer;
113     private float mCornerRadius = 0f;
114     private int mBackgroundColorFloating;
115 
116     @Nullable private Bubble mBubble;
117     private PendingIntent mPendingIntent;
118     // TODO(b/170891664): Don't use a flag, set the BubbleOverflow object instead
119     private boolean mIsOverflow;
120 
121     private BubbleController mController;
122     private BubbleStackView mStackView;
123     private BubblePositioner mPositioner;
124 
125     /**
126      * Container for the {@code TaskView} that has a solid, round-rect background that shows if the
127      * {@code TaskView} hasn't loaded.
128      */
129     private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext());
130 
131     private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
132         private boolean mInitialized = false;
133         private boolean mDestroyed = false;
134 
135         @Override
136         public void onInitialized() {
137             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
138                 Log.d(TAG, "onInitialized: destroyed=" + mDestroyed
139                         + " initialized=" + mInitialized
140                         + " bubble=" + getBubbleKey());
141             }
142 
143             if (mDestroyed || mInitialized) {
144                 return;
145             }
146 
147             // Custom options so there is no activity transition animation
148             ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(),
149                     0 /* enterResId */, 0 /* exitResId */);
150 
151             Rect launchBounds = new Rect();
152             mTaskView.getBoundsOnScreen(launchBounds);
153 
154             // TODO: I notice inconsistencies in lifecycle
155             // Post to keep the lifecycle normal
156             post(() -> {
157                 if (DEBUG_BUBBLE_EXPANDED_VIEW) {
158                     Log.d(TAG, "onInitialized: calling startActivity, bubble="
159                             + getBubbleKey());
160                 }
161                 try {
162                     options.setTaskAlwaysOnTop(true);
163                     options.setLaunchedFromBubble(true);
164                     if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
165                         options.setApplyActivityFlagsForBubbles(true);
166                         mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
167                                 options, launchBounds);
168                     } else {
169                         Intent fillInIntent = new Intent();
170                         // Apply flags to make behaviour match documentLaunchMode=always.
171                         fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
172                         fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
173                         if (mBubble != null) {
174                             mBubble.setIntentActive();
175                         }
176                         mTaskView.startActivity(mPendingIntent, fillInIntent, options,
177                                 launchBounds);
178                     }
179                 } catch (RuntimeException e) {
180                     // If there's a runtime exception here then there's something
181                     // wrong with the intent, we can't really recover / try to populate
182                     // the bubble again so we'll just remove it.
183                     Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey()
184                             + ", " + e.getMessage() + "; removing bubble");
185                     mController.removeBubble(getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT);
186                 }
187             });
188             mInitialized = true;
189         }
190 
191         @Override
192         public void onReleased() {
193             mDestroyed = true;
194         }
195 
196         @Override
197         public void onTaskCreated(int taskId, ComponentName name) {
198             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
199                 Log.d(TAG, "onTaskCreated: taskId=" + taskId
200                         + " bubble=" + getBubbleKey());
201             }
202             // The taskId is saved to use for removeTask, preventing appearance in recent tasks.
203             mTaskId = taskId;
204 
205             // With the task org, the taskAppeared callback will only happen once the task has
206             // already drawn
207             setContentVisibility(true);
208         }
209 
210         @Override
211         public void onTaskVisibilityChanged(int taskId, boolean visible) {
212             setContentVisibility(visible);
213         }
214 
215         @Override
216         public void onTaskRemovalStarted(int taskId) {
217             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
218                 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId
219                         + " bubble=" + getBubbleKey());
220             }
221             if (mBubble != null) {
222                 // Must post because this is called from a binder thread.
223                 post(() -> mController.removeBubble(
224                         mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED));
225             }
226         }
227 
228         @Override
229         public void onBackPressedOnTaskRoot(int taskId) {
230             if (mTaskId == taskId && mStackView.isExpanded()) {
231                 mStackView.onBackPressed();
232             }
233         }
234     };
235 
BubbleExpandedView(Context context)236     public BubbleExpandedView(Context context) {
237         this(context, null);
238     }
239 
BubbleExpandedView(Context context, AttributeSet attrs)240     public BubbleExpandedView(Context context, AttributeSet attrs) {
241         this(context, attrs, 0);
242     }
243 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)244     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
245         this(context, attrs, defStyleAttr, 0);
246     }
247 
BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)248     public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
249             int defStyleRes) {
250         super(context, attrs, defStyleAttr, defStyleRes);
251     }
252 
253     @SuppressLint("ClickableViewAccessibility")
254     @Override
onFinishInflate()255     protected void onFinishInflate() {
256         super.onFinishInflate();
257         mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate(
258                 R.layout.bubble_manage_button, this /* parent */, false /* attach */);
259         updateDimensions();
260         mPointerView = findViewById(R.id.pointer_view);
261         mCurrentPointer = mTopPointer;
262         mPointerView.setVisibility(INVISIBLE);
263 
264         // Set {@code TaskView}'s alpha value as zero, since there is no view content to be shown.
265         setContentVisibility(false);
266 
267         mExpandedViewContainer.setOutlineProvider(new ViewOutlineProvider() {
268             @Override
269             public void getOutline(View view, Outline outline) {
270                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
271             }
272         });
273         mExpandedViewContainer.setClipToOutline(true);
274         mExpandedViewContainer.setLayoutParams(
275                 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
276         addView(mExpandedViewContainer);
277 
278         // Expanded stack layout, top to bottom:
279         // Expanded view container
280         // ==> bubble row
281         // ==> expanded view
282         //   ==> activity view
283         //   ==> manage button
284         bringChildToFront(mManageButton);
285 
286         applyThemeAttrs();
287 
288         setClipToPadding(false);
289         setOnTouchListener((view, motionEvent) -> {
290             if (mTaskView == null) {
291                 return false;
292             }
293 
294             final Rect avBounds = new Rect();
295             mTaskView.getBoundsOnScreen(avBounds);
296 
297             // Consume and ignore events on the expanded view padding that are within the
298             // {@code TaskView}'s vertical bounds. These events are part of a back gesture, and so
299             // they should not collapse the stack (which all other touches on areas around the AV
300             // would do).
301             if (motionEvent.getRawY() >= avBounds.top
302                             && motionEvent.getRawY() <= avBounds.bottom
303                             && (motionEvent.getRawX() < avBounds.left
304                                 || motionEvent.getRawX() > avBounds.right)) {
305                 return true;
306             }
307 
308             return false;
309         });
310 
311         // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout
312         // so the Manage button appears on the right.
313         setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
314     }
315 
316     /**
317      * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need
318      * to be called after view inflate.
319      */
initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow)320     void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) {
321         mController = controller;
322         mStackView = stackView;
323         mIsOverflow = isOverflow;
324         mPositioner = mController.getPositioner();
325 
326         if (mIsOverflow) {
327             mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
328                     R.layout.bubble_overflow_container, null /* root */);
329             mOverflowView.setBubbleController(mController);
330             FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
331             mExpandedViewContainer.addView(mOverflowView, lp);
332             mExpandedViewContainer.setLayoutParams(
333                     new LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
334             bringChildToFront(mOverflowView);
335             mManageButton.setVisibility(GONE);
336         } else {
337             mTaskView = new TaskView(mContext, mController.getTaskOrganizer(),
338                     mController.getSyncTransactionQueue());
339             mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
340             mExpandedViewContainer.addView(mTaskView);
341             bringChildToFront(mTaskView);
342         }
343     }
344 
updateDimensions()345     void updateDimensions() {
346         Resources res = getResources();
347         updateFontSize();
348 
349         mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
350         mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
351         mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius);
352         mPointerEffect = new CornerPathEffect(mPointerRadius);
353         mPointerOverlap = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_overlap);
354         mTopPointer = new ShapeDrawable(TriangleShape.create(
355                 mPointerWidth, mPointerHeight, true /* pointUp */));
356         mLeftPointer = new ShapeDrawable(TriangleShape.createHorizontal(
357                 mPointerWidth, mPointerHeight, true /* pointLeft */));
358         mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal(
359                 mPointerWidth, mPointerHeight, false /* pointLeft */));
360         if (mPointerView != null) {
361             updatePointerView();
362         }
363 
364         if (mManageButton != null) {
365             int visibility = mManageButton.getVisibility();
366             removeView(mManageButton);
367             mManageButton = (AlphaOptimizedButton) LayoutInflater.from(getContext()).inflate(
368                     R.layout.bubble_manage_button, this /* parent */, false /* attach */);
369             addView(mManageButton);
370             mManageButton.setVisibility(visibility);
371         }
372     }
373 
updateFontSize()374     void updateFontSize() {
375         final float fontSize = mContext.getResources()
376                 .getDimensionPixelSize(com.android.internal.R.dimen.text_size_body_2_material);
377         if (mManageButton != null) {
378             mManageButton.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
379         }
380         if (mOverflowView != null) {
381             mOverflowView.updateFontSize();
382         }
383     }
384 
applyThemeAttrs()385     void applyThemeAttrs() {
386         final TypedArray ta = mContext.obtainStyledAttributes(new int[] {
387                 android.R.attr.dialogCornerRadius,
388                 android.R.attr.colorBackgroundFloating});
389         mCornerRadius = ta.getDimensionPixelSize(0, 0);
390         mBackgroundColorFloating = ta.getColor(1, Color.WHITE);
391         mExpandedViewContainer.setBackgroundColor(mBackgroundColorFloating);
392         ta.recycle();
393 
394         if (mTaskView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
395                 mContext.getResources())) {
396             mTaskView.setCornerRadius(mCornerRadius);
397         }
398         updatePointerView();
399     }
400 
401     /** Updates the size and visuals of the pointer. **/
updatePointerView()402     private void updatePointerView() {
403         LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams();
404         if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) {
405             lp.width = mPointerHeight;
406             lp.height = mPointerWidth;
407         } else {
408             lp.width = mPointerWidth;
409             lp.height = mPointerHeight;
410         }
411         mCurrentPointer.setTint(mBackgroundColorFloating);
412 
413         Paint arrowPaint = mCurrentPointer.getPaint();
414         arrowPaint.setColor(mBackgroundColorFloating);
415         arrowPaint.setPathEffect(mPointerEffect);
416         mPointerView.setLayoutParams(lp);
417         mPointerView.setBackground(mCurrentPointer);
418     }
419 
getBubbleKey()420     private String getBubbleKey() {
421         return mBubble != null ? mBubble.getKey() : "null";
422     }
423 
424     /**
425      * Sets whether the surface displaying app content should sit on top. This is useful for
426      * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
427      * being dragged out, the manage menu) this is set to false, otherwise it should be true.
428      */
setSurfaceZOrderedOnTop(boolean onTop)429     void setSurfaceZOrderedOnTop(boolean onTop) {
430         if (mTaskView == null) {
431             return;
432         }
433         mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
434     }
435 
setImeVisible(boolean visible)436     void setImeVisible(boolean visible) {
437         mImeVisible = visible;
438         if (!mImeVisible && mNeedsNewHeight) {
439             updateHeight();
440         }
441     }
442 
443     /** Return a GraphicBuffer with the contents of the task view surface. */
444     @Nullable
snapshotActivitySurface()445     SurfaceControl.ScreenshotHardwareBuffer snapshotActivitySurface() {
446         if (mIsOverflow) {
447             // For now, just snapshot the view and return it as a hw buffer so that the animation
448             // code for both the tasks and overflow can be the same
449             Picture p = new Picture();
450             mOverflowView.draw(
451                     p.beginRecording(mOverflowView.getWidth(), mOverflowView.getHeight()));
452             p.endRecording();
453             Bitmap snapshot = Bitmap.createBitmap(p);
454             return new SurfaceControl.ScreenshotHardwareBuffer(snapshot.getHardwareBuffer(),
455                     snapshot.getColorSpace(), false /* containsSecureLayers */);
456         }
457         if (mTaskView == null || mTaskView.getSurfaceControl() == null) {
458             return null;
459         }
460         return SurfaceControl.captureLayers(
461                 mTaskView.getSurfaceControl(),
462                 new Rect(0, 0, mTaskView.getWidth(), mTaskView.getHeight()),
463                 1 /* scale */);
464     }
465 
getTaskViewLocationOnScreen()466     int[] getTaskViewLocationOnScreen() {
467         if (mIsOverflow) {
468             // This is only used for animating away the surface when switching bubbles, just use the
469             // view location on screen for now to allow us to use the same animation code with tasks
470             return mOverflowView.getLocationOnScreen();
471         }
472         if (mTaskView != null) {
473             return mTaskView.getLocationOnScreen();
474         } else {
475             return new int[]{0, 0};
476         }
477     }
478 
479     // TODO: Could listener be passed when we pass StackView / can we avoid setting this like this
setManageClickListener(OnClickListener manageClickListener)480     void setManageClickListener(OnClickListener manageClickListener) {
481         mManageButton.setOnClickListener(manageClickListener);
482     }
483 
484     /**
485      * Updates the obscured touchable region for the task surface. This calls onLocationChanged,
486      * which results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is
487      * useful if a view has been added or removed from on top of the {@code TaskView}, such as the
488      * manage menu.
489      */
updateObscuredTouchableRegion()490     void updateObscuredTouchableRegion() {
491         if (mTaskView != null) {
492             mTaskView.onLocationChanged();
493         }
494     }
495 
496     @Override
onDetachedFromWindow()497     protected void onDetachedFromWindow() {
498         super.onDetachedFromWindow();
499         mImeVisible = false;
500         mNeedsNewHeight = false;
501         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
502             Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey());
503         }
504     }
505 
506     /**
507      * Whether we are currently animating the {@code TaskView}'s alpha value. If this is set to
508      * true, calls to {@link #setContentVisibility} will not be applied until this is set to false
509      * again.
510      */
setAlphaAnimating(boolean animating)511     void setAlphaAnimating(boolean animating) {
512         mIsAlphaAnimating = animating;
513 
514         // If we're done animating, apply the correct
515         if (!animating) {
516             setContentVisibility(mIsContentVisible);
517         }
518     }
519 
520     /**
521      * Sets the alpha of the underlying {@code TaskView}, since changing the expanded view's alpha
522      * does not affect the {@code TaskView} since it uses a Surface.
523      */
setTaskViewAlpha(float alpha)524     void setTaskViewAlpha(float alpha) {
525         if (mTaskView != null) {
526             mTaskView.setAlpha(alpha);
527         }
528         mPointerView.setAlpha(alpha);
529         setAlpha(alpha);
530     }
531 
532     /**
533      * Set visibility of contents in the expanded state.
534      *
535      * @param visibility {@code true} if the contents should be visible on the screen.
536      *
537      * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
538      * and setting {@code false} actually means rendering the contents in transparent.
539      */
setContentVisibility(boolean visibility)540     void setContentVisibility(boolean visibility) {
541         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
542             Log.d(TAG, "setContentVisibility: visibility=" + visibility
543                     + " bubble=" + getBubbleKey());
544         }
545         mIsContentVisible = visibility;
546         if (mTaskView != null && !mIsAlphaAnimating) {
547             mTaskView.setAlpha(visibility ? 1f : 0f);
548             mPointerView.setAlpha(visibility ? 1f : 0f);
549         }
550     }
551 
552     @Nullable
getTaskView()553     TaskView getTaskView() {
554         return mTaskView;
555     }
556 
getTaskId()557     int getTaskId() {
558         return mTaskId;
559     }
560 
561     /**
562      * Sets the bubble used to populate this view.
563      */
update(Bubble bubble)564     void update(Bubble bubble) {
565         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
566             Log.d(TAG, "update: bubble=" + bubble);
567         }
568         if (mStackView == null) {
569             Log.w(TAG, "Stack is null for bubble: " + bubble);
570             return;
571         }
572         boolean isNew = mBubble == null || didBackingContentChange(bubble);
573         if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
574             mBubble = bubble;
575             mManageButton.setContentDescription(getResources().getString(
576                     R.string.bubbles_settings_button_description, bubble.getAppName()));
577             mManageButton.setAccessibilityDelegate(
578                     new AccessibilityDelegate() {
579                         @Override
580                         public void onInitializeAccessibilityNodeInfo(View host,
581                                 AccessibilityNodeInfo info) {
582                             super.onInitializeAccessibilityNodeInfo(host, info);
583                             // On focus, have TalkBack say
584                             // "Actions available. Use swipe up then right to view."
585                             // in addition to the default "double tap to activate".
586                             mStackView.setupLocalMenu(info);
587                         }
588                     });
589 
590             if (isNew) {
591                 mPendingIntent = mBubble.getBubbleIntent();
592                 if ((mPendingIntent != null || mBubble.hasMetadataShortcutId())
593                         && mTaskView != null) {
594                     setContentVisibility(false);
595                     mTaskView.setVisibility(VISIBLE);
596                 }
597             }
598             applyThemeAttrs();
599         } else {
600             Log.w(TAG, "Trying to update entry with different key, new bubble: "
601                     + bubble.getKey() + " old bubble: " + bubble.getKey());
602         }
603     }
604 
605     /**
606      * Bubbles are backed by a pending intent or a shortcut, once the activity is
607      * started we never change it / restart it on notification updates -- unless the bubbles'
608      * backing data switches.
609      *
610      * This indicates if the new bubble is backed by a different data source than what was
611      * previously shown here (e.g. previously a pending intent & now a shortcut).
612      *
613      * @param newBubble the bubble this view is being updated with.
614      * @return true if the backing content has changed.
615      */
didBackingContentChange(Bubble newBubble)616     private boolean didBackingContentChange(Bubble newBubble) {
617         boolean prevWasIntentBased = mBubble != null && mPendingIntent != null;
618         boolean newIsIntentBased = newBubble.getBubbleIntent() != null;
619         return prevWasIntentBased != newIsIntentBased;
620     }
621 
updateHeight()622     void updateHeight() {
623         if (mExpandedViewContainerLocation == null) {
624             return;
625         }
626 
627         if ((mBubble != null && mTaskView != null) || mIsOverflow) {
628             float desiredHeight = mPositioner.getExpandedViewHeight(mBubble);
629             int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow);
630             float height = desiredHeight == MAX_HEIGHT
631                     ? maxHeight
632                     : Math.min(desiredHeight, maxHeight);
633             FrameLayout.LayoutParams lp = mIsOverflow
634                     ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams()
635                     : (FrameLayout.LayoutParams) mTaskView.getLayoutParams();
636             mNeedsNewHeight = lp.height != height;
637             if (!mImeVisible) {
638                 // If the ime is visible... don't adjust the height because that will cause
639                 // a configuration change and the ime will be lost.
640                 lp.height = (int) height;
641                 if (mIsOverflow) {
642                     mOverflowView.setLayoutParams(lp);
643                 } else {
644                     mTaskView.setLayoutParams(lp);
645                 }
646                 mNeedsNewHeight = false;
647             }
648             if (DEBUG_BUBBLE_EXPANDED_VIEW) {
649                 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()
650                         + " height=" + height
651                         + " mNeedsNewHeight=" + mNeedsNewHeight);
652             }
653         }
654     }
655 
656     /**
657      * Update appearance of the expanded view being displayed.
658      *
659      * @param containerLocationOnScreen The location on-screen of the container the expanded view is
660      *                                  added to. This allows us to calculate max height without
661      *                                  waiting for layout.
662      */
updateView(int[] containerLocationOnScreen)663     public void updateView(int[] containerLocationOnScreen) {
664         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
665             Log.d(TAG, "updateView: bubble="
666                     + getBubbleKey());
667         }
668         mExpandedViewContainerLocation = containerLocationOnScreen;
669         updateHeight();
670         if (mTaskView != null
671                 && mTaskView.getVisibility() == VISIBLE
672                 && mTaskView.isAttachedToWindow()) {
673             mTaskView.onLocationChanged();
674         }
675         if (mIsOverflow) {
676             mOverflowView.show();
677         }
678     }
679 
680     /**
681      * Sets the position of the pointer.
682      *
683      * When bubbles are showing "vertically" they display along the left / right sides of the
684      * screen with the expanded view beside them.
685      *
686      * If they aren't showing vertically they're positioned along the top of the screen with the
687      * expanded view below them.
688      *
689      * @param bubblePosition the x position of the bubble if showing on top, the y position of
690      *                       the bubble if showing vertically.
691      * @param onLeft whether the stack was on the left side of the screen when expanded.
692      */
setPointerPosition(float bubblePosition, boolean onLeft, boolean animate)693     public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) {
694         // Pointer gets drawn in the padding
695         final boolean showVertically = mPositioner.showBubblesVertically();
696         final float paddingLeft = (showVertically && onLeft)
697                 ? mPointerHeight - mPointerOverlap
698                 : 0;
699         final float paddingRight = (showVertically && !onLeft)
700                 ? mPointerHeight - mPointerOverlap
701                 : 0;
702         final float paddingTop = showVertically
703                 ? 0
704                 : mPointerHeight - mPointerOverlap;
705         setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0);
706 
707         // Subtract the expandedViewY here because the pointer is placed within the expandedView.
708         float pointerPosition = mPositioner.getPointerPosition(bubblePosition);
709         final float bubbleCenter = mPositioner.showBubblesVertically()
710                 ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition)
711                 : pointerPosition;
712         // Post because we need the width of the view
713         post(() -> {
714             mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
715             updatePointerView();
716             float pointerY;
717             float pointerX;
718             if (showVertically) {
719                 pointerY = bubbleCenter - (mPointerWidth / 2f);
720                 pointerX = onLeft
721                         ? -mPointerHeight + mPointerOverlap
722                         : getWidth() - mPaddingRight - mPointerOverlap;
723             } else {
724                 pointerY = mPointerOverlap;
725                 pointerX = bubbleCenter - (mPointerWidth / 2f);
726             }
727             if (animate) {
728                 mPointerView.animate().translationX(pointerX).translationY(pointerY).start();
729             } else {
730                 mPointerView.setTranslationY(pointerY);
731                 mPointerView.setTranslationX(pointerX);
732                 mPointerView.setVisibility(VISIBLE);
733             }
734         });
735     }
736 
737     /**
738      * Position of the manage button displayed in the expanded view. Used for placing user
739      * education about the manage button.
740      */
getManageButtonBoundsOnScreen(Rect rect)741     public void getManageButtonBoundsOnScreen(Rect rect) {
742         mManageButton.getBoundsOnScreen(rect);
743     }
744 
getManageButtonMargin()745     public int getManageButtonMargin() {
746         return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart();
747     }
748 
749     /**
750      * Cleans up anything related to the task and {@code TaskView}. If this view should be reused
751      * after this method is called, then
752      * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first.
753      */
cleanUpExpandedState()754     public void cleanUpExpandedState() {
755         if (DEBUG_BUBBLE_EXPANDED_VIEW) {
756             Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId);
757         }
758         if (getTaskId() != INVALID_TASK_ID) {
759             try {
760                 ActivityTaskManager.getService().removeTask(getTaskId());
761             } catch (RemoteException e) {
762                 Log.w(TAG, e.getMessage());
763             }
764         }
765         if (mTaskView != null) {
766             mTaskView.release();
767             removeView(mTaskView);
768             mTaskView = null;
769         }
770     }
771 
772     /**
773      * Description of current expanded view state.
774      */
dump( @onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)775     public void dump(
776             @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
777         pw.print("BubbleExpandedView");
778         pw.print("  taskId:               "); pw.println(mTaskId);
779         pw.print("  stackView:            "); pw.println(mStackView);
780     }
781 }
782