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 
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.graphics.Rect;
25 import android.graphics.Region;
26 import android.graphics.drawable.ColorDrawable;
27 import android.view.TouchDelegate;
28 import android.view.View;
29 import android.view.ViewTreeObserver;
30 import android.widget.FrameLayout;
31 
32 import com.android.wm.shell.bubbles.BubbleController;
33 import com.android.wm.shell.bubbles.BubbleOverflow;
34 import com.android.wm.shell.bubbles.BubblePositioner;
35 import com.android.wm.shell.bubbles.BubbleViewProvider;
36 
37 import java.util.function.Consumer;
38 
39 /**
40  * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window
41  * manager to display bubbles. However, it is only used when bubbles are being displayed in
42  * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around
43  * on screen and instead shows & animates the expanded bubble for the bubble bar.
44  */
45 public class BubbleBarLayerView extends FrameLayout
46         implements ViewTreeObserver.OnComputeInternalInsetsListener {
47 
48     private static final String TAG = BubbleBarLayerView.class.getSimpleName();
49 
50     private static final float SCRIM_ALPHA = 0.2f;
51 
52     private final BubbleController mBubbleController;
53     private final BubblePositioner mPositioner;
54     private final BubbleBarAnimationHelper mAnimationHelper;
55     private final BubbleEducationViewController mEducationViewController;
56     private final View mScrimView;
57 
58     @Nullable
59     private BubbleViewProvider mExpandedBubble;
60     private BubbleBarExpandedView mExpandedView;
61     private @Nullable Consumer<String> mUnBubbleConversationCallback;
62 
63     // TODO(b/273310265) - currently the view is always on the right, need to update for RTL.
64     /** Whether the expanded view is displaying on the left of the screen or not. */
65     private boolean mOnLeft = false;
66 
67     /** Whether a bubble is expanded. */
68     private boolean mIsExpanded = false;
69 
70     private final Region mTouchableRegion = new Region();
71     private final Rect mTempRect = new Rect();
72 
73     // Used to ensure touch target size for the menu shown on a bubble expanded view
74     private TouchDelegate mHandleTouchDelegate;
75     private final Rect mHandleTouchBounds = new Rect();
76 
BubbleBarLayerView(Context context, BubbleController controller)77     public BubbleBarLayerView(Context context, BubbleController controller) {
78         super(context);
79         mBubbleController = controller;
80         mPositioner = mBubbleController.getPositioner();
81 
82         mAnimationHelper = new BubbleBarAnimationHelper(context,
83                 this, mPositioner);
84         mEducationViewController = new BubbleEducationViewController(context, (boolean visible) -> {
85             if (mExpandedView == null) return;
86             mExpandedView.setObscured(visible);
87         });
88 
89         mScrimView = new View(getContext());
90         mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
91         mScrimView.setBackgroundDrawable(new ColorDrawable(
92                 getResources().getColor(android.R.color.system_neutral1_1000)));
93         addView(mScrimView);
94         mScrimView.setAlpha(0f);
95         mScrimView.setBackgroundDrawable(new ColorDrawable(
96                 getResources().getColor(android.R.color.system_neutral1_1000)));
97 
98         setOnClickListener(view -> hideMenuOrCollapse());
99     }
100 
101     @Override
onAttachedToWindow()102     protected void onAttachedToWindow() {
103         super.onAttachedToWindow();
104         mPositioner.update();
105         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
106     }
107 
108     @Override
onDetachedFromWindow()109     protected void onDetachedFromWindow() {
110         super.onDetachedFromWindow();
111         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
112 
113         if (mExpandedView != null) {
114             mEducationViewController.hideManageEducation(/* animated = */ false);
115             removeView(mExpandedView);
116             mExpandedView = null;
117         }
118     }
119 
120     @Override
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)121     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
122         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
123         mTouchableRegion.setEmpty();
124         getTouchableRegion(mTouchableRegion);
125         inoutInfo.touchableRegion.set(mTouchableRegion);
126     }
127 
128     /** Updates the sizes of any displaying expanded view. */
onDisplaySizeChanged()129     public void onDisplaySizeChanged() {
130         if (mIsExpanded && mExpandedView != null) {
131             updateExpandedView();
132         }
133     }
134 
135     /** Whether the stack of bubbles is expanded or not. */
isExpanded()136     public boolean isExpanded() {
137         return mIsExpanded;
138     }
139 
140     // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done.
141     /** Whether the expanded view is positioned on the left or right side of the screen. */
isOnLeft()142     public boolean isOnLeft() {
143         return mOnLeft;
144     }
145 
146     /** Shows the expanded view of the provided bubble. */
showExpandedView(BubbleViewProvider b)147     public void showExpandedView(BubbleViewProvider b) {
148         BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
149         if (expandedView == null) {
150             return;
151         }
152         if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) {
153             removeView(mExpandedView);
154             mExpandedView = null;
155         }
156         if (mExpandedView == null) {
157             if (expandedView.getParent() != null) {
158                 // Expanded view might be animating collapse and is still attached
159                 // Cancel current animations and remove from parent
160                 mAnimationHelper.cancelAnimations();
161                 removeView(expandedView);
162             }
163             mExpandedBubble = b;
164             mExpandedView = expandedView;
165             boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY);
166             final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
167             final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
168             mExpandedView.setVisibility(GONE);
169             mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
170             mExpandedView.setLayerBoundsSupplier(() -> new Rect(0, 0, getWidth(), getHeight()));
171             mExpandedView.setListener(new BubbleBarExpandedView.Listener() {
172                 @Override
173                 public void onTaskCreated() {
174                     mEducationViewController.maybeShowManageEducation(b, mExpandedView);
175                 }
176 
177                 @Override
178                 public void onUnBubbleConversation(String bubbleKey) {
179                     if (mUnBubbleConversationCallback != null) {
180                         mUnBubbleConversationCallback.accept(bubbleKey);
181                     }
182                 }
183 
184                 @Override
185                 public void onBackPressed() {
186                     hideMenuOrCollapse();
187                 }
188             });
189 
190             addView(mExpandedView, new FrameLayout.LayoutParams(width, height));
191         }
192 
193         mIsExpanded = true;
194         mBubbleController.getSysuiProxy().onStackExpandChanged(true);
195         mAnimationHelper.animateExpansion(mExpandedBubble, () -> {
196             if (mExpandedView == null) return;
197             // Touch delegate for the menu
198             BubbleBarHandleView view = mExpandedView.getHandleView();
199             view.getBoundsOnScreen(mHandleTouchBounds);
200             mHandleTouchBounds.top -= mPositioner.getBubblePaddingTop();
201             mHandleTouchDelegate = new TouchDelegate(mHandleTouchBounds,
202                     mExpandedView.getHandleView());
203             setTouchDelegate(mHandleTouchDelegate);
204         });
205 
206         showScrim(true);
207     }
208 
209     /** Collapses any showing expanded view */
collapse()210     public void collapse() {
211         mIsExpanded = false;
212         final BubbleBarExpandedView viewToRemove = mExpandedView;
213         mEducationViewController.hideManageEducation(/* animated = */ true);
214         mAnimationHelper.animateCollapse(() -> removeView(viewToRemove));
215         mBubbleController.getSysuiProxy().onStackExpandChanged(false);
216         mExpandedView = null;
217         setTouchDelegate(null);
218         showScrim(false);
219     }
220 
221     /** Sets the function to call to un-bubble the given conversation. */
setUnBubbleConversationCallback( @ullable Consumer<String> unBubbleConversationCallback)222     public void setUnBubbleConversationCallback(
223             @Nullable Consumer<String> unBubbleConversationCallback) {
224         mUnBubbleConversationCallback = unBubbleConversationCallback;
225     }
226 
227     /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */
hideMenuOrCollapse()228     private void hideMenuOrCollapse() {
229         if (mEducationViewController.isManageEducationVisible()) {
230             mEducationViewController.hideManageEducation(/* animated = */ true);
231         } else if (isExpanded() && mExpandedView != null) {
232             mExpandedView.hideMenuOrCollapse();
233         } else {
234             mBubbleController.collapseStack();
235         }
236     }
237 
238     /** Updates the expanded view size and position. */
updateExpandedView()239     private void updateExpandedView() {
240         if (mExpandedView == null) return;
241         boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
242         final int padding = mPositioner.getBubbleBarExpandedViewPadding();
243         final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
244         final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
245         FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
246         lp.width = width;
247         lp.height = height;
248         mExpandedView.setLayoutParams(lp);
249         if (mOnLeft) {
250             mExpandedView.setX(mPositioner.getInsets().left + padding);
251         } else {
252             mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
253         }
254         mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
255         mExpandedView.updateLocation();
256     }
257 
showScrim(boolean show)258     private void showScrim(boolean show) {
259         if (show) {
260             mScrimView.animate()
261                     .setInterpolator(ALPHA_IN)
262                     .alpha(SCRIM_ALPHA)
263                     .start();
264         } else {
265             mScrimView.animate()
266                     .alpha(0f)
267                     .setInterpolator(ALPHA_OUT)
268                     .start();
269         }
270     }
271 
272     /**
273      * Fills in the touchable region for expanded view. This is used by window manager to
274      * decide which touch events go to the expanded view.
275      */
getTouchableRegion(Region outRegion)276     private void getTouchableRegion(Region outRegion) {
277         mTempRect.setEmpty();
278         if (mIsExpanded) {
279             getBoundsOnScreen(mTempRect);
280             outRegion.op(mTempRect, Region.Op.UNION);
281         }
282     }
283 }
284