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 com.android.wm.shell.animation.Interpolators.ALPHA_IN;
20 import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
21 import static com.android.wm.shell.bubbles.Bubbles.DISMISS_USER_GESTURE;
22 
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.graphics.Region;
28 import android.graphics.drawable.ColorDrawable;
29 import android.view.Gravity;
30 import android.view.TouchDelegate;
31 import android.view.View;
32 import android.view.ViewTreeObserver;
33 import android.view.WindowManager;
34 import android.widget.FrameLayout;
35 
36 import androidx.annotation.NonNull;
37 
38 import com.android.wm.shell.bubbles.Bubble;
39 import com.android.wm.shell.bubbles.BubbleController;
40 import com.android.wm.shell.bubbles.BubbleData;
41 import com.android.wm.shell.bubbles.BubbleOverflow;
42 import com.android.wm.shell.bubbles.BubblePositioner;
43 import com.android.wm.shell.bubbles.BubbleViewProvider;
44 import com.android.wm.shell.bubbles.DeviceConfig;
45 import com.android.wm.shell.bubbles.DismissViewUtils;
46 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener;
47 import com.android.wm.shell.common.bubbles.BaseBubblePinController;
48 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
49 import com.android.wm.shell.common.bubbles.DismissView;
50 
51 import kotlin.Unit;
52 
53 import java.util.Objects;
54 import java.util.function.Consumer;
55 
56 /**
57  * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window
58  * manager to display bubbles. However, it is only used when bubbles are being displayed in
59  * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around
60  * on screen and instead shows & animates the expanded bubble for the bubble bar.
61  */
62 public class BubbleBarLayerView extends FrameLayout
63         implements ViewTreeObserver.OnComputeInternalInsetsListener {
64 
65     private static final String TAG = BubbleBarLayerView.class.getSimpleName();
66 
67     private static final float SCRIM_ALPHA = 0.2f;
68 
69     private final BubbleController mBubbleController;
70     private final BubbleData mBubbleData;
71     private final BubblePositioner mPositioner;
72     private final BubbleBarAnimationHelper mAnimationHelper;
73     private final BubbleEducationViewController mEducationViewController;
74     private final View mScrimView;
75     private final BubbleExpandedViewPinController mBubbleExpandedViewPinController;
76 
77     @Nullable
78     private BubbleViewProvider mExpandedBubble;
79     @Nullable
80     private BubbleBarExpandedView mExpandedView;
81     @Nullable
82     private BubbleBarExpandedViewDragController mDragController;
83     private DismissView mDismissView;
84     private @Nullable Consumer<String> mUnBubbleConversationCallback;
85 
86     /** Whether a bubble is expanded. */
87     private boolean mIsExpanded = false;
88 
89     private final Region mTouchableRegion = new Region();
90     private final Rect mTempRect = new Rect();
91 
92     // Used to ensure touch target size for the menu shown on a bubble expanded view
93     private TouchDelegate mHandleTouchDelegate;
94     private final Rect mHandleTouchBounds = new Rect();
95 
BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData)96     public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData) {
97         super(context);
98         mBubbleController = controller;
99         mBubbleData = bubbleData;
100         mPositioner = mBubbleController.getPositioner();
101 
102         mAnimationHelper = new BubbleBarAnimationHelper(context,
103                 this, mPositioner);
104         mEducationViewController = new BubbleEducationViewController(context, (boolean visible) -> {
105             if (mExpandedView == null) return;
106             mExpandedView.setObscured(visible);
107         });
108 
109         mScrimView = new View(getContext());
110         mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
111         mScrimView.setBackgroundDrawable(new ColorDrawable(
112                 getResources().getColor(android.R.color.system_neutral1_1000)));
113         addView(mScrimView);
114         mScrimView.setAlpha(0f);
115         mScrimView.setBackgroundDrawable(new ColorDrawable(
116                 getResources().getColor(android.R.color.system_neutral1_1000)));
117 
118         setUpDismissView();
119 
120         mBubbleExpandedViewPinController = new BubbleExpandedViewPinController(
121                 context, this, mPositioner);
122         mBubbleExpandedViewPinController.setListener(
123                 new BaseBubblePinController.LocationChangeListener() {
124                     @Override
125                     public void onChange(@NonNull BubbleBarLocation bubbleBarLocation) {
126                         mBubbleController.animateBubbleBarLocation(bubbleBarLocation);
127                     }
128 
129                     @Override
130                     public void onRelease(@NonNull BubbleBarLocation location) {
131                         mBubbleController.setBubbleBarLocation(location);
132                     }
133                 });
134 
135         setOnClickListener(view -> hideModalOrCollapse());
136     }
137 
138     @Override
onAttachedToWindow()139     protected void onAttachedToWindow() {
140         super.onAttachedToWindow();
141         WindowManager windowManager = mContext.getSystemService(WindowManager.class);
142         mPositioner.update(DeviceConfig.create(mContext, Objects.requireNonNull(windowManager)));
143         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
144     }
145 
146     @Override
onDetachedFromWindow()147     protected void onDetachedFromWindow() {
148         super.onDetachedFromWindow();
149         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
150 
151         if (mExpandedView != null) {
152             mEducationViewController.hideEducation(/* animated = */ false);
153             removeView(mExpandedView);
154             mExpandedView = null;
155         }
156     }
157 
158     @Override
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)159     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
160         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
161         mTouchableRegion.setEmpty();
162         getTouchableRegion(mTouchableRegion);
163         inoutInfo.touchableRegion.set(mTouchableRegion);
164     }
165 
166     /** Updates the sizes of any displaying expanded view. */
onDisplaySizeChanged()167     public void onDisplaySizeChanged() {
168         if (mIsExpanded && mExpandedView != null) {
169             updateExpandedView();
170         }
171     }
172 
173     /** Whether the stack of bubbles is expanded or not. */
isExpanded()174     public boolean isExpanded() {
175         return mIsExpanded;
176     }
177 
178     /** Shows the expanded view of the provided bubble. */
showExpandedView(BubbleViewProvider b)179     public void showExpandedView(BubbleViewProvider b) {
180         BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
181         if (expandedView == null) {
182             return;
183         }
184         if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
185             removeView(mExpandedView);
186             mExpandedView = null;
187         }
188         if (mExpandedView == null) {
189             if (expandedView.getParent() != null) {
190                 // Expanded view might be animating collapse and is still attached
191                 // Cancel current animations and remove from parent
192                 mAnimationHelper.cancelAnimations();
193                 removeView(expandedView);
194             }
195             mExpandedBubble = b;
196             mExpandedView = expandedView;
197             boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY);
198             final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
199             final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
200             mExpandedView.setVisibility(GONE);
201             mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
202             mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
203             mExpandedView.setListener(new BubbleBarExpandedView.Listener() {
204                 @Override
205                 public void onTaskCreated() {
206                     if (mEducationViewController != null && mExpandedView != null) {
207                         mEducationViewController.maybeShowManageEducation(b, mExpandedView);
208                     }
209                 }
210 
211                 @Override
212                 public void onUnBubbleConversation(String bubbleKey) {
213                     if (mUnBubbleConversationCallback != null) {
214                         mUnBubbleConversationCallback.accept(bubbleKey);
215                     }
216                 }
217 
218                 @Override
219                 public void onBackPressed() {
220                     hideModalOrCollapse();
221                 }
222             });
223 
224             DragListener dragListener = inDismiss -> {
225                 if (inDismiss && mExpandedBubble != null) {
226                     mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE);
227                 }
228             };
229             mDragController = new BubbleBarExpandedViewDragController(
230                     mExpandedView,
231                     mDismissView,
232                     mAnimationHelper,
233                     mPositioner,
234                     mBubbleExpandedViewPinController,
235                     dragListener);
236 
237             addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT));
238         }
239 
240         if (mEducationViewController.isEducationVisible()) {
241             mEducationViewController.hideEducation(/* animated = */ true);
242         }
243 
244         mIsExpanded = true;
245         mBubbleController.getSysuiProxy().onStackExpandChanged(true);
246         mAnimationHelper.animateExpansion(mExpandedBubble, () -> {
247             if (mExpandedView == null) return;
248             // Touch delegate for the menu
249             BubbleBarHandleView view = mExpandedView.getHandleView();
250             view.getBoundsOnScreen(mHandleTouchBounds);
251             // Move top value up to ensure touch target is large enough
252             mHandleTouchBounds.top -= mPositioner.getBubblePaddingTop();
253             mHandleTouchDelegate = new TouchDelegate(mHandleTouchBounds,
254                     mExpandedView.getHandleView());
255             setTouchDelegate(mHandleTouchDelegate);
256         });
257 
258         showScrim(true);
259     }
260 
261     /** Removes the given {@code bubble}. */
removeBubble(Bubble bubble, Runnable endAction)262     public void removeBubble(Bubble bubble, Runnable endAction) {
263         Runnable cleanUp = () -> {
264             bubble.cleanupViews();
265             endAction.run();
266         };
267         if (mBubbleData.getBubbles().isEmpty()) {
268             // we're removing the last bubble. collapse the expanded view and cleanup bubble views
269             // at the end.
270             collapse(cleanUp);
271         } else {
272             cleanUp.run();
273         }
274     }
275 
276     /** Collapses any showing expanded view */
collapse()277     public void collapse() {
278         collapse(/* endAction= */ null);
279     }
280 
281     /**
282      * Collapses any showing expanded view.
283      *
284      * @param endAction an action to run and the end of the collapse animation.
285      */
collapse(@ullable Runnable endAction)286     public void collapse(@Nullable Runnable endAction) {
287         if (!mIsExpanded) {
288             if (endAction != null) {
289                 endAction.run();
290             }
291             return;
292         }
293         mIsExpanded = false;
294         final BubbleBarExpandedView viewToRemove = mExpandedView;
295         mEducationViewController.hideEducation(/* animated = */ true);
296         Runnable runnable = () -> {
297             removeView(viewToRemove);
298             if (endAction != null) {
299                 endAction.run();
300             }
301             if (mBubbleData.getBubbles().isEmpty()) {
302                 mBubbleController.onAllBubblesAnimatedOut();
303             }
304         };
305         if (mDragController != null && mDragController.isStuckToDismiss()) {
306             mAnimationHelper.animateDismiss(runnable);
307         } else {
308             mAnimationHelper.animateCollapse(runnable);
309         }
310         mBubbleController.getSysuiProxy().onStackExpandChanged(false);
311         mExpandedView = null;
312         mDragController = null;
313         setTouchDelegate(null);
314         showScrim(false);
315     }
316 
317     /**
318      * Show bubble bar user education relative to the reference position.
319      * @param position the reference position in Screen coordinates.
320      */
showUserEducation(Point position)321     public void showUserEducation(Point position) {
322         mEducationViewController.showStackEducation(position, /* root = */ this, () -> {
323             // When the user education is clicked hide it and expand the selected bubble
324             mEducationViewController.hideEducation(/* animated = */ true, () -> {
325                 mBubbleController.expandStackWithSelectedBubble();
326                 return Unit.INSTANCE;
327             });
328             return Unit.INSTANCE;
329         });
330     }
331 
332     /** Sets the function to call to un-bubble the given conversation. */
setUnBubbleConversationCallback( @ullable Consumer<String> unBubbleConversationCallback)333     public void setUnBubbleConversationCallback(
334             @Nullable Consumer<String> unBubbleConversationCallback) {
335         mUnBubbleConversationCallback = unBubbleConversationCallback;
336     }
337 
setUpDismissView()338     private void setUpDismissView() {
339         if (mDismissView != null) {
340             removeView(mDismissView);
341         }
342         mDismissView = new DismissView(getContext());
343         DismissViewUtils.setup(mDismissView);
344         addView(mDismissView);
345     }
346 
347     /** Hides the current modal education/menu view, IME or collapses the expanded view */
hideModalOrCollapse()348     private void hideModalOrCollapse() {
349         if (mEducationViewController.isEducationVisible()) {
350             mEducationViewController.hideEducation(/* animated = */ true);
351             return;
352         }
353         if (isExpanded() && mExpandedView != null) {
354             boolean menuHidden = mExpandedView.hideMenuIfVisible();
355             if (menuHidden) {
356                 return;
357             }
358             boolean imeHidden = mExpandedView.hideImeIfVisible();
359             if (imeHidden) {
360                 return;
361             }
362         }
363         mBubbleController.collapseStack();
364     }
365 
366     /** Updates the expanded view size and position. */
updateExpandedView()367     public void updateExpandedView() {
368         if (mExpandedView == null || mExpandedBubble == null) return;
369         boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
370         mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(),
371                 isOverflowExpanded, mTempRect);
372         FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
373         lp.width = mTempRect.width();
374         lp.height = mTempRect.height();
375         mExpandedView.setLayoutParams(lp);
376         mExpandedView.setX(mTempRect.left);
377         mExpandedView.setY(mTempRect.top);
378         mExpandedView.updateLocation();
379     }
380 
showScrim(boolean show)381     private void showScrim(boolean show) {
382         if (show) {
383             mScrimView.animate()
384                     .setInterpolator(ALPHA_IN)
385                     .alpha(SCRIM_ALPHA)
386                     .start();
387         } else {
388             mScrimView.animate()
389                     .alpha(0f)
390                     .setInterpolator(ALPHA_OUT)
391                     .start();
392         }
393     }
394 
395     /**
396      * Fills in the touchable region for expanded view. This is used by window manager to
397      * decide which touch events go to the expanded view.
398      */
getTouchableRegion(Region outRegion)399     private void getTouchableRegion(Region outRegion) {
400         mTempRect.setEmpty();
401         if (mIsExpanded || mEducationViewController.isEducationVisible()) {
402             getBoundsOnScreen(mTempRect);
403             outRegion.op(mTempRect, Region.Op.UNION);
404         }
405     }
406 
407 }
408