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