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