• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.bar;
18 
19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
20 
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.graphics.Insets;
24 import android.graphics.Outline;
25 import android.graphics.Rect;
26 import android.os.Bundle;
27 import android.util.AttributeSet;
28 import android.util.FloatProperty;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewOutlineProvider;
33 import android.view.accessibility.AccessibilityNodeInfo;
34 import android.widget.FrameLayout;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.wm.shell.R;
40 import com.android.wm.shell.bubbles.Bubble;
41 import com.android.wm.shell.bubbles.BubbleExpandedViewManager;
42 import com.android.wm.shell.bubbles.BubbleLogger;
43 import com.android.wm.shell.bubbles.BubbleOverflowContainerView;
44 import com.android.wm.shell.bubbles.BubblePositioner;
45 import com.android.wm.shell.bubbles.BubbleTaskView;
46 import com.android.wm.shell.bubbles.BubbleTaskViewListener;
47 import com.android.wm.shell.bubbles.Bubbles;
48 import com.android.wm.shell.bubbles.RegionSamplingProvider;
49 import com.android.wm.shell.dagger.HasWMComponent;
50 import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
51 import com.android.wm.shell.shared.handles.RegionSamplingHelper;
52 import com.android.wm.shell.taskview.TaskView;
53 
54 import java.util.concurrent.Executor;
55 import java.util.function.Supplier;
56 
57 import javax.inject.Inject;
58 
59 /** Expanded view of a bubble when it's part of the bubble bar. */
60 public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewListener.Callback {
61     /**
62      * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal
63      * actions and events
64      */
65     public interface Listener {
66         /** Called when the task view task is first created. */
onTaskCreated()67         void onTaskCreated();
68         /** Called when expanded view needs to un-bubble the given conversation */
onUnBubbleConversation(String bubbleKey)69         void onUnBubbleConversation(String bubbleKey);
70         /** Called when expanded view task view back button pressed */
onBackPressed()71         void onBackPressed();
72     }
73 
74     /**
75      * A property wrapper around corner radius for the expanded view, handled by
76      * {@link #setCornerRadius(float)} and {@link #getCornerRadius()} methods.
77      */
78     public static final FloatProperty<BubbleBarExpandedView> CORNER_RADIUS = new FloatProperty<>(
79             "cornerRadius") {
80         @Override
81         public void setValue(BubbleBarExpandedView bbev, float radius) {
82             bbev.setCornerRadius(radius);
83         }
84 
85         @Override
86         public Float get(BubbleBarExpandedView bbev) {
87             return bbev.getCornerRadius();
88         }
89     };
90 
91     /**
92      * Property to set alpha for the task view
93      */
94     public static final FloatProperty<BubbleBarExpandedView> TASK_VIEW_ALPHA = new FloatProperty<>(
95             "taskViewAlpha") {
96         @Override
97         public void setValue(BubbleBarExpandedView bbev, float alpha) {
98             bbev.setTaskViewAlpha(alpha);
99         }
100 
101         @Override
102         public Float get(BubbleBarExpandedView bbev) {
103             return bbev.mTaskView != null ? bbev.mTaskView.getAlpha() : bbev.getAlpha();
104         }
105     };
106 
107     private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
108     private static final int INVALID_TASK_ID = -1;
109 
110     private Bubble mBubble;
111     private BubbleExpandedViewManager mManager;
112     private BubblePositioner mPositioner;
113     private boolean mIsOverflow;
114     private BubbleTaskViewListener mBubbleTaskViewListener;
115     private BubbleBarMenuViewController mMenuViewController;
116     @Nullable
117     private Supplier<Rect> mLayerBoundsSupplier;
118     @Nullable
119     private Listener mListener;
120 
121     private BubbleBarHandleView mHandleView;
122     @Nullable
123     private TaskView mTaskView;
124     @Nullable
125     private BubbleOverflowContainerView mOverflowView;
126 
127     /**
128      * The handle shown in the caption area is tinted based on the background color of the area.
129      * This can vary so we sample the caption region and update the handle color based on that.
130      * If we're showing the overflow, the helper and executors will be null.
131      */
132     @Nullable
133     private RegionSamplingHelper mRegionSamplingHelper;
134     @Nullable
135     private RegionSamplingProvider mRegionSamplingProvider;
136     @Nullable
137     private Executor mMainExecutor;
138     @Nullable
139     private Executor mBackgroundExecutor;
140     private final Rect mSampleRect = new Rect();
141     private final int[] mLoc = new int[2];
142     private final Rect mTempBounds = new Rect();
143 
144     /** Height of the caption inset at the top of the TaskView */
145     private int mCaptionHeight;
146     /** Corner radius used when view is resting */
147     private float mRestingCornerRadius = 0f;
148     /** Corner radius applied while dragging */
149     private float mDraggedCornerRadius = 0f;
150     /** Current corner radius */
151     private float mCurrentCornerRadius = 0f;
152 
153     /** A runnable to start the expansion animation as soon as the task view is made visible. */
154     @Nullable
155     private Runnable mAnimateExpansion = null;
156     private TaskViewVisibilityState mVisibilityState = TaskViewVisibilityState.INVISIBLE;
157 
158     /**
159      * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
160      * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
161      * value until the animation ends.
162      */
163     private boolean mIsContentVisible = false;
164     private boolean mIsAnimating;
165     private boolean mIsDragging;
166 
167     private boolean mIsClipping = false;
168     private int mBottomClip = 0;
169 
170     /** An enum value that tracks the visibility state of the task view */
171     private enum TaskViewVisibilityState {
172         /** The task view is going away, and we're waiting for the surface to be destroyed. */
173         PENDING_INVISIBLE,
174         /** The task view is invisible and does not have a surface. */
175         INVISIBLE,
176         /** The task view is in the process of being added to a surface. */
177         PENDING_VISIBLE,
178         /** The task view is visible and has a surface. */
179         VISIBLE
180     }
181 
182     // Ideally this would be package private, but we have to set this in a fake for test and we
183     // don't yet have dagger set up for tests, so have to set manually
184     @VisibleForTesting
185     @Inject
186     public BubbleLogger bubbleLogger;
187 
BubbleBarExpandedView(Context context)188     public BubbleBarExpandedView(Context context) {
189         this(context, null);
190     }
191 
BubbleBarExpandedView(Context context, AttributeSet attrs)192     public BubbleBarExpandedView(Context context, AttributeSet attrs) {
193         this(context, attrs, 0);
194     }
195 
BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr)196     public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
197         this(context, attrs, defStyleAttr, 0);
198     }
199 
BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)200     public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
201             int defStyleRes) {
202         super(context, attrs, defStyleAttr, defStyleRes);
203     }
204 
205     @Override
onFinishInflate()206     protected void onFinishInflate() {
207         super.onFinishInflate();
208         Context context = getContext();
209         if (context instanceof HasWMComponent) {
210             ((HasWMComponent) context).getWMComponent().inject(this);
211         }
212         setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
213         mCaptionHeight = context.getResources().getDimensionPixelSize(
214                 R.dimen.bubble_bar_expanded_view_caption_height);
215         mHandleView = findViewById(R.id.bubble_bar_handle_view);
216         applyThemeAttrs();
217         setClipToOutline(true);
218         setOutlineProvider(new ViewOutlineProvider() {
219             @Override
220             public void getOutline(View view, Outline outline) {
221                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight() - mBottomClip,
222                         mCurrentCornerRadius);
223             }
224         });
225         // Set a touch sink to ensure that clicks on the caption area do not propagate to the parent
226         setOnTouchListener((v, event) -> true);
227     }
228 
229     /** Initializes the view, must be called before doing anything else. */
initialize(BubbleExpandedViewManager expandedViewManager, BubblePositioner positioner, boolean isOverflow, @Nullable BubbleTaskView bubbleTaskView, @Nullable Executor mainExecutor, @Nullable Executor backgroundExecutor, @Nullable RegionSamplingProvider regionSamplingProvider)230     public void initialize(BubbleExpandedViewManager expandedViewManager,
231             BubblePositioner positioner,
232             boolean isOverflow,
233             @Nullable BubbleTaskView bubbleTaskView,
234             @Nullable Executor mainExecutor,
235             @Nullable Executor backgroundExecutor,
236             @Nullable RegionSamplingProvider regionSamplingProvider) {
237         mManager = expandedViewManager;
238         mPositioner = positioner;
239         mIsOverflow = isOverflow;
240         mMainExecutor = mainExecutor;
241         mBackgroundExecutor = backgroundExecutor;
242         mRegionSamplingProvider = regionSamplingProvider;
243 
244         if (mIsOverflow) {
245             mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
246                     R.layout.bubble_overflow_container, null /* root */);
247             mOverflowView.initialize(expandedViewManager, positioner);
248             addView(mOverflowView);
249             // Don't show handle for overflow
250             mHandleView.setVisibility(View.GONE);
251         } else {
252             mTaskView = bubbleTaskView.getTaskView();
253             mBubbleTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView,
254                     /* viewParent= */ this,
255                     expandedViewManager,
256                     /* callback= */ this);
257 
258             // if the task view is already attached to a parent we need to remove it
259             if (mTaskView.getParent() != null) {
260                 // it's possible that the task view is visible, e.g. if we're unfolding, in which
261                 // case removing it will trigger a visibility change. we have to wait for that
262                 // signal before we can add it to this expanded view, otherwise the signal will be
263                 // incorrect because the task view will have a surface.
264                 // if the task view is not visible, then it has no surface and removing it will not
265                 // trigger any visibility change signals.
266                 if (bubbleTaskView.isVisible()) {
267                     mVisibilityState = TaskViewVisibilityState.PENDING_INVISIBLE;
268                 }
269                 ((ViewGroup) mTaskView.getParent()).removeView(mTaskView);
270             }
271 
272             // if we're invisible it's safe to setup the task view and then await on the visibility
273             // signal.
274             if (mVisibilityState == TaskViewVisibilityState.INVISIBLE) {
275                 mVisibilityState = TaskViewVisibilityState.PENDING_VISIBLE;
276                 setupTaskView();
277             }
278 
279             // Handle view needs to draw on top of task view.
280             bringChildToFront(mHandleView);
281 
282             mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate());
283         }
284         mMenuViewController = new BubbleBarMenuViewController(mContext, mHandleView, this);
285         mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
286             @Override
287             public void onMenuVisibilityChanged(boolean visible) {
288                 setObscured(visible);
289                 if (visible) {
290                     mHandleView.setFocusable(false);
291                     mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
292                 } else {
293                     mHandleView.setFocusable(true);
294                     mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO);
295                 }
296             }
297 
298             @Override
299             public void onUnBubbleConversation(Bubble bubble) {
300                 if (mListener != null) {
301                     mListener.onUnBubbleConversation(bubble.getKey());
302                 }
303                 bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_OPT_OUT);
304             }
305 
306             @Override
307             public void onOpenAppSettings(Bubble bubble) {
308                 mManager.collapseStack();
309                 mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser());
310                 bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_GO_TO_SETTINGS);
311             }
312 
313             @Override
314             public void onDismissBubble(Bubble bubble) {
315                 mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE);
316                 bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU);
317             }
318 
319             @Override
320             public void onMoveToFullscreen(Bubble bubble) {
321                 if (mTaskView != null) {
322                     mTaskView.moveToFullscreen();
323                 }
324             }
325         });
326         mHandleView.setOnClickListener(view -> {
327             mMenuViewController.showMenu(true /* animated */);
328         });
329     }
330 
setupTaskView()331     private void setupTaskView() {
332         FrameLayout.LayoutParams lp =
333                 new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
334         addView(mTaskView, lp);
335         mTaskView.setEnableSurfaceClipping(true);
336         mTaskView.setCornerRadius(mCurrentCornerRadius);
337         mTaskView.setVisibility(VISIBLE);
338         mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0));
339     }
340 
getHandleView()341     public BubbleBarHandleView getHandleView() {
342         return mHandleView;
343     }
344 
345     /** Updates the view based on the current theme. */
applyThemeAttrs()346     public void applyThemeAttrs() {
347         mCaptionHeight = getResources().getDimensionPixelSize(
348                 R.dimen.bubble_bar_expanded_view_caption_height);
349         mRestingCornerRadius = getResources().getDimensionPixelSize(
350                 R.dimen.bubble_bar_expanded_view_corner_radius);
351         mDraggedCornerRadius = getResources().getDimensionPixelSize(
352                 R.dimen.bubble_bar_expanded_view_corner_radius_dragged);
353 
354         mCurrentCornerRadius = mRestingCornerRadius;
355 
356         if (mTaskView != null) {
357             mTaskView.setCornerRadius(mCurrentCornerRadius);
358             mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0));
359         }
360     }
361 
362     @Override
onDetachedFromWindow()363     protected void onDetachedFromWindow() {
364         super.onDetachedFromWindow();
365         // Hide manage menu when view disappears
366         mMenuViewController.hideMenu(false /* animated */);
367         if (mRegionSamplingHelper != null) {
368             mRegionSamplingHelper.stopAndDestroy();
369         }
370     }
371 
372     @Override
onAttachedToWindow()373     protected void onAttachedToWindow() {
374         super.onAttachedToWindow();
375         recreateRegionSamplingHelper();
376     }
377 
378     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)379     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
380         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
381         if (mTaskView != null) {
382             int height = MeasureSpec.getSize(heightMeasureSpec);
383             measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height,
384                     MeasureSpec.getMode(heightMeasureSpec)));
385         }
386     }
387 
388     @Override
onLayout(boolean changed, int l, int t, int r, int b)389     protected void onLayout(boolean changed, int l, int t, int r, int b) {
390         super.onLayout(changed, l, t, r, b);
391         if (mTaskView != null) {
392             mTaskView.layout(l, t, r, t + mTaskView.getMeasuredHeight());
393         }
394     }
395 
396     @Override
onTaskCreated()397     public void onTaskCreated() {
398         if (mTaskView != null) {
399             mTaskView.setAlpha(0);
400         }
401         if (mListener != null) {
402             mListener.onTaskCreated();
403         }
404         // when the task is created we're visible
405         onTaskViewVisible();
406     }
407 
408     @Override
onContentVisibilityChanged(boolean visible)409     public void onContentVisibilityChanged(boolean visible) {
410         if (mVisibilityState == TaskViewVisibilityState.PENDING_INVISIBLE && !visible) {
411             // the surface is now destroyed. set up the task view and wait for the visibility
412             // signal.
413             mVisibilityState = TaskViewVisibilityState.PENDING_VISIBLE;
414             setupTaskView();
415             return;
416         }
417         if (visible) {
418             onTaskViewVisible();
419         }
420     }
421 
422     @Override
onTaskRemovalStarted()423     public void onTaskRemovalStarted() {
424         if (mRegionSamplingHelper != null) {
425             mRegionSamplingHelper.stopAndDestroy();
426         }
427     }
428 
429     @Override
onBackPressed()430     public void onBackPressed() {
431         if (mListener == null) return;
432         mListener.onBackPressed();
433     }
434 
animateExpansionWhenTaskViewVisible(Runnable animateExpansion)435     void animateExpansionWhenTaskViewVisible(Runnable animateExpansion) {
436         if (mVisibilityState == TaskViewVisibilityState.VISIBLE || mIsOverflow) {
437             animateExpansion.run();
438         } else {
439             mAnimateExpansion = animateExpansion;
440         }
441     }
442 
onTaskViewVisible()443     private void onTaskViewVisible() {
444         // if we're waiting to be visible, start the expansion animation if it's pending.
445         if (mVisibilityState == TaskViewVisibilityState.PENDING_VISIBLE) {
446             mVisibilityState = TaskViewVisibilityState.VISIBLE;
447             if (mAnimateExpansion != null) {
448                 mAnimateExpansion.run();
449                 mAnimateExpansion = null;
450             }
451         }
452     }
453 
454     /**
455      * Set whether this view is currently being dragged.
456      *
457      * When dragging, the handle is hidden and content shouldn't be sampled. When dragging has
458      * ended we should start again.
459      */
setDragging(boolean isDragging)460     public void setDragging(boolean isDragging) {
461         if (isDragging != mIsDragging) {
462             mIsDragging = isDragging;
463             updateSamplingState();
464 
465             if (isDragging && mPositioner.isImeVisible()) {
466                 // Hide the IME when dragging begins
467                 mManager.hideCurrentInputMethod();
468             }
469         }
470     }
471 
472     /** Returns whether region sampling should be enabled, i.e. if task view content is visible. */
shouldSampleRegion()473     private boolean shouldSampleRegion() {
474         return mTaskView != null
475                 && mTaskView.getTaskInfo() != null
476                 && !mIsDragging
477                 && !mIsAnimating
478                 && mIsContentVisible;
479     }
480 
481     /**
482      * Handles starting or stopping the region sampling helper based on
483      * {@link #shouldSampleRegion()}.
484      */
updateSamplingState()485     private void updateSamplingState() {
486         if (mRegionSamplingHelper == null) return;
487         boolean shouldSample = shouldSampleRegion();
488         if (shouldSample) {
489             mRegionSamplingHelper.start(getCaptionSampleRect());
490         } else {
491             mRegionSamplingHelper.stop();
492         }
493     }
494 
495     /** Returns the current area of the caption bar, in screen coordinates. */
getCaptionSampleRect()496     Rect getCaptionSampleRect() {
497         if (mTaskView == null) return null;
498         mTaskView.getLocationOnScreen(mLoc);
499         mSampleRect.set(mLoc[0], mLoc[1],
500                 mLoc[0] + mTaskView.getWidth(),
501                 mLoc[1] + mCaptionHeight);
502         return mSampleRect;
503     }
504 
505     @VisibleForTesting
506     @Nullable
getRegionSamplingHelper()507     public RegionSamplingHelper getRegionSamplingHelper() {
508         return mRegionSamplingHelper;
509     }
510 
511     /** Cleans up the expanded view, should be called when the bubble is no longer active. */
cleanUpExpandedState()512     public void cleanUpExpandedState() {
513         mMenuViewController.hideMenu(false /* animated */);
514     }
515 
516     /**
517      * Hides the current modal menu if it is visible
518      * @return {@code true} if menu was visible and is hidden
519      */
hideMenuIfVisible()520     public boolean hideMenuIfVisible() {
521         if (mMenuViewController.isMenuVisible()) {
522             mMenuViewController.hideMenu(true /* animated */);
523             return true;
524         }
525         return false;
526     }
527 
528     /**
529      * Hides the IME if it is visible
530      * @return {@code true} if IME was visible
531      */
hideImeIfVisible()532     public boolean hideImeIfVisible() {
533         if (mPositioner.isImeVisible()) {
534             mManager.hideCurrentInputMethod();
535             return true;
536         }
537         return false;
538     }
539 
540     /** Updates the bubble shown in the expanded view. */
update(Bubble bubble)541     public void update(Bubble bubble) {
542         mBubble = bubble;
543         mBubbleTaskViewListener.setBubble(bubble);
544         mMenuViewController.updateMenu(bubble);
545     }
546 
547     /** The task id of the activity shown in the task view, if it exists. */
getTaskId()548     public int getTaskId() {
549         return mBubbleTaskViewListener != null
550                 ? mBubbleTaskViewListener.getTaskId()
551                 : INVALID_TASK_ID;
552     }
553 
554     /** Sets layer bounds supplier used for obscured touchable region of task view */
setLayerBoundsSupplier(@ullable Supplier<Rect> supplier)555     void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) {
556         mLayerBoundsSupplier = supplier;
557     }
558 
559     /** Sets expanded view listener */
setListener(@ullable Listener listener)560     void setListener(@Nullable Listener listener) {
561         mListener = listener;
562     }
563 
564     /** Sets whether the view is obscured by some modal view */
setObscured(boolean obscured)565     void setObscured(boolean obscured) {
566         if (mTaskView == null || mLayerBoundsSupplier == null) return;
567         // Updates the obscured touchable region for the task surface.
568         mTaskView.setObscuredTouchRect(obscured ? mLayerBoundsSupplier.get() : null);
569     }
570 
571     /**
572      * Call when the location or size of the view has changed to update TaskView.
573      */
updateLocation()574     public void updateLocation() {
575         if (mTaskView != null) {
576             mTaskView.onLocationChanged();
577         }
578     }
579 
580     /** Shows the expanded view for the overflow if it exists. */
maybeShowOverflow()581     void maybeShowOverflow() {
582         if (mOverflowView != null) {
583             // post this to the looper so that the view has a chance to be laid out before it can
584             // calculate row and column sizes correctly.
585             post(() -> mOverflowView.show());
586         }
587     }
588 
589     /** Sets the alpha of the task view. */
setContentVisibility(boolean visible)590     public void setContentVisibility(boolean visible) {
591         mIsContentVisible = visible;
592 
593         if (mTaskView == null) return;
594 
595         if (!mIsAnimating) {
596             mTaskView.setAlpha(visible ? 1f : 0f);
597             if (mRegionSamplingHelper != null) {
598                 mRegionSamplingHelper.setWindowVisible(visible);
599             }
600             updateSamplingState();
601         }
602     }
603 
604     /**
605      * Sets the alpha of both this view and the task view.
606      */
setTaskViewAlpha(float alpha)607     public void setTaskViewAlpha(float alpha) {
608         if (mTaskView != null) {
609             mTaskView.setAlpha(alpha);
610         }
611         setAlpha(alpha);
612     }
613 
614     /**
615      * Sets whether the surface displaying app content should sit on top. This is useful for
616      * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
617      * being dragged out, the manage menu) this is set to false, otherwise it should be true.
618      */
setSurfaceZOrderedOnTop(boolean onTop)619     public void setSurfaceZOrderedOnTop(boolean onTop) {
620         if (mTaskView == null) {
621             return;
622         }
623         mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
624     }
625 
626     @VisibleForTesting
isSurfaceZOrderedOnTop()627     boolean isSurfaceZOrderedOnTop() {
628         return mTaskView != null && mTaskView.isZOrderedOnTop();
629     }
630 
631     /**
632      * Sets whether the view is animating, in this case we won't change the content visibility
633      * until the animation is done.
634      */
setAnimating(boolean animating)635     public void setAnimating(boolean animating) {
636         mIsAnimating = animating;
637         if (mIsAnimating) {
638             // Stop sampling while animating -- when animating is done setContentVisibility will
639             // re-trigger sampling if we're visible.
640             updateSamplingState();
641         }
642         // If we're done animating, apply the correct visibility.
643         if (!animating) {
644             setContentVisibility(mIsContentVisible);
645         }
646     }
647 
648     /**
649      * Check whether the view is animating
650      */
isAnimating()651     public boolean isAnimating() {
652         return mIsAnimating;
653     }
654 
655     /** @return corner radius that should be applied while view is in rest */
getRestingCornerRadius()656     public float getRestingCornerRadius() {
657         return mRestingCornerRadius;
658     }
659 
660     /** @return corner radius that should be applied while view is being dragged */
getDraggedCornerRadius()661     public float getDraggedCornerRadius() {
662         return mDraggedCornerRadius;
663     }
664 
665     /** @return current corner radius */
getCornerRadius()666     public float getCornerRadius() {
667         return mCurrentCornerRadius;
668     }
669 
670     /** Update corner radius */
setCornerRadius(float cornerRadius)671     public void setCornerRadius(float cornerRadius) {
672         if (mCurrentCornerRadius != cornerRadius) {
673             mCurrentCornerRadius = cornerRadius;
674             if (mTaskView != null) {
675                 mTaskView.setCornerRadius(cornerRadius);
676             }
677             invalidateOutline();
678         }
679     }
680 
681     /** The y coordinate of the bottom of the expanded view. */
getContentBottomOnScreen()682     public int getContentBottomOnScreen() {
683         if (mOverflowView != null) {
684             mOverflowView.getBoundsOnScreen(mTempBounds);
685         }
686         if (mTaskView != null) {
687             mTaskView.getBoundsOnScreen(mTempBounds);
688         }
689         return mTempBounds.bottom;
690     }
691 
692     /** Update the amount by which to clip the expanded view at the bottom. */
updateBottomClip(int bottomClip)693     public void updateBottomClip(int bottomClip) {
694         mBottomClip = bottomClip;
695         onClipUpdate();
696     }
697 
onClipUpdate()698     private void onClipUpdate() {
699         if (mBottomClip == 0) {
700             if (mIsClipping) {
701                 mIsClipping = false;
702                 if (mTaskView != null) {
703                     mTaskView.setClipBounds(null);
704                     mTaskView.setEnableSurfaceClipping(false);
705                 }
706                 invalidateOutline();
707             }
708         } else {
709             if (!mIsClipping) {
710                 mIsClipping = true;
711                 if (mTaskView != null) {
712                     mTaskView.setEnableSurfaceClipping(true);
713                 }
714             }
715             invalidateOutline();
716             if (mTaskView != null) {
717                 Rect clipBounds = new Rect(0, 0,
718                         mTaskView.getWidth(),
719                         mTaskView.getHeight() - mBottomClip);
720                 mTaskView.setClipBounds(clipBounds);
721             }
722         }
723     }
724 
recreateRegionSamplingHelper()725     private void recreateRegionSamplingHelper() {
726         if (mRegionSamplingHelper != null) {
727             mRegionSamplingHelper.stopAndDestroy();
728         }
729         if (mMainExecutor == null || mBackgroundExecutor == null
730                 || mRegionSamplingProvider == null) {
731             // Null when it's the overflow / don't need sampling then.
732             return;
733         }
734         mRegionSamplingHelper = mRegionSamplingProvider.createHelper(this,
735                 new RegionSamplingHelper.SamplingCallback() {
736                     @Override
737                     public void onRegionDarknessChanged(boolean isRegionDark) {
738                         if (mHandleView != null) {
739                             mHandleView.updateHandleColor(isRegionDark,
740                                     true /* animated */);
741                         }
742                     }
743 
744                     @Override
745                     public Rect getSampledRegion(View sampledView) {
746                         return getCaptionSampleRect();
747                     }
748 
749                     @Override
750                     public boolean isSamplingEnabled() {
751                         return shouldSampleRegion();
752                     }
753                 }, mMainExecutor, mBackgroundExecutor);
754     }
755 
756     private class HandleViewAccessibilityDelegate extends AccessibilityDelegate {
757         @Override
onInitializeAccessibilityNodeInfo(@onNull View host, @NonNull AccessibilityNodeInfo info)758         public void onInitializeAccessibilityNodeInfo(@NonNull View host,
759                 @NonNull AccessibilityNodeInfo info) {
760             super.onInitializeAccessibilityNodeInfo(host, info);
761             info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
762                     AccessibilityNodeInfo.ACTION_CLICK, getResources().getString(
763                     R.string.bubble_accessibility_action_expand_menu)));
764             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
765             info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
766             if (mPositioner.isBubbleBarOnLeft()) {
767                 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
768                         R.id.action_move_bubble_bar_right, getResources().getString(
769                         R.string.bubble_accessibility_action_move_bar_right)));
770             } else {
771                 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
772                         R.id.action_move_bubble_bar_left, getResources().getString(
773                         R.string.bubble_accessibility_action_move_bar_left)));
774             }
775         }
776 
777         @Override
performAccessibilityAction(@onNull View host, int action, @Nullable Bundle args)778         public boolean performAccessibilityAction(@NonNull View host, int action,
779                 @Nullable Bundle args) {
780             if (super.performAccessibilityAction(host, action, args)) {
781                 return true;
782             }
783             if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
784                 mManager.collapseStack();
785                 return true;
786             }
787             if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
788                 mManager.dismissBubble(mBubble, Bubbles.DISMISS_USER_GESTURE);
789                 return true;
790             }
791             if (action == R.id.action_move_bubble_bar_left) {
792                 mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT,
793                         BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW);
794                 return true;
795             }
796             if (action == R.id.action_move_bubble_bar_right) {
797                 mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT,
798                         BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW);
799                 return true;
800             }
801             return false;
802         }
803     }
804 }
805