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 android.annotation.Nullable;
20 import android.app.ActivityManager;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Color;
24 import android.graphics.Insets;
25 import android.graphics.Outline;
26 import android.graphics.Rect;
27 import android.util.AttributeSet;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewOutlineProvider;
31 import android.widget.FrameLayout;
32 
33 import com.android.internal.policy.ScreenDecorationsUtils;
34 import com.android.wm.shell.R;
35 import com.android.wm.shell.bubbles.Bubble;
36 import com.android.wm.shell.bubbles.BubbleController;
37 import com.android.wm.shell.bubbles.BubbleOverflowContainerView;
38 import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
39 import com.android.wm.shell.bubbles.Bubbles;
40 import com.android.wm.shell.taskview.TaskView;
41 
42 import java.util.function.Supplier;
43 
44 /**
45  * Expanded view of a bubble when it's part of the bubble bar.
46  *
47  * {@link BubbleController#isShowingAsBubbleBar()}
48  */
49 public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener {
50     /**
51      * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal
52      * actions and events
53      */
54     public interface Listener {
55         /** Called when the task view task is first created. */
onTaskCreated()56         void onTaskCreated();
57         /** Called when expanded view needs to un-bubble the given conversation */
onUnBubbleConversation(String bubbleKey)58         void onUnBubbleConversation(String bubbleKey);
59         /** Called when expanded view task view back button pressed */
onBackPressed()60         void onBackPressed();
61     }
62 
63     private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
64     private static final int INVALID_TASK_ID = -1;
65 
66     private BubbleController mController;
67     private boolean mIsOverflow;
68     private BubbleTaskViewHelper mBubbleTaskViewHelper;
69     private BubbleBarMenuViewController mMenuViewController;
70     private @Nullable Supplier<Rect> mLayerBoundsSupplier;
71     private @Nullable Listener mListener;
72 
73     private BubbleBarHandleView mHandleView = new BubbleBarHandleView(getContext());
74     private @Nullable TaskView mTaskView;
75     private @Nullable BubbleOverflowContainerView mOverflowView;
76 
77     private int mCaptionHeight;
78 
79     private int mBackgroundColor;
80     private float mCornerRadius = 0f;
81 
82     /**
83      * Whether we want the {@code TaskView}'s content to be visible (alpha = 1f). If
84      * {@link #mIsAnimating} is true, this may not reflect the {@code TaskView}'s actual alpha
85      * value until the animation ends.
86      */
87     private boolean mIsContentVisible = false;
88     private boolean mIsAnimating;
89 
BubbleBarExpandedView(Context context)90     public BubbleBarExpandedView(Context context) {
91         this(context, null);
92     }
93 
BubbleBarExpandedView(Context context, AttributeSet attrs)94     public BubbleBarExpandedView(Context context, AttributeSet attrs) {
95         this(context, attrs, 0);
96     }
97 
BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr)98     public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr) {
99         this(context, attrs, defStyleAttr, 0);
100     }
101 
BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)102     public BubbleBarExpandedView(Context context, AttributeSet attrs, int defStyleAttr,
103             int defStyleRes) {
104         super(context, attrs, defStyleAttr, defStyleRes);
105     }
106 
107     @Override
onFinishInflate()108     protected void onFinishInflate() {
109         super.onFinishInflate();
110         Context context = getContext();
111         setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation));
112         mCaptionHeight = context.getResources().getDimensionPixelSize(
113                 R.dimen.bubble_bar_expanded_view_caption_height);
114         addView(mHandleView);
115         applyThemeAttrs();
116         setClipToOutline(true);
117         setOutlineProvider(new ViewOutlineProvider() {
118             @Override
119             public void getOutline(View view, Outline outline) {
120                 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
121             }
122         });
123     }
124 
125     @Override
onDetachedFromWindow()126     protected void onDetachedFromWindow() {
127         super.onDetachedFromWindow();
128         // Hide manage menu when view disappears
129         mMenuViewController.hideMenu(false /* animated */);
130     }
131 
132     /** Set the BubbleController on the view, must be called before doing anything else. */
initialize(BubbleController controller, boolean isOverflow)133     public void initialize(BubbleController controller, boolean isOverflow) {
134         mController = controller;
135         mIsOverflow = isOverflow;
136 
137         if (mIsOverflow) {
138             mOverflowView = (BubbleOverflowContainerView) LayoutInflater.from(getContext()).inflate(
139                     R.layout.bubble_overflow_container, null /* root */);
140             mOverflowView.setBubbleController(mController);
141             addView(mOverflowView);
142         } else {
143 
144             mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
145                     /* listener= */ this,
146                     /* viewParent= */ this);
147             mTaskView = mBubbleTaskViewHelper.getTaskView();
148             addView(mTaskView);
149             mTaskView.setEnableSurfaceClipping(true);
150             mTaskView.setCornerRadius(mCornerRadius);
151 
152             // Handle view needs to draw on top of task view.
153             bringChildToFront(mHandleView);
154         }
155         mMenuViewController = new BubbleBarMenuViewController(mContext, this);
156         mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
157             @Override
158             public void onMenuVisibilityChanged(boolean visible) {
159                 setObscured(visible);
160             }
161 
162             @Override
163             public void onUnBubbleConversation(Bubble bubble) {
164                 if (mListener != null) {
165                     mListener.onUnBubbleConversation(bubble.getKey());
166                 }
167             }
168 
169             @Override
170             public void onOpenAppSettings(Bubble bubble) {
171                 mController.collapseStack();
172                 mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser());
173             }
174 
175             @Override
176             public void onDismissBubble(Bubble bubble) {
177                 mController.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
178             }
179         });
180         mHandleView.setOnClickListener(view -> {
181             mMenuViewController.showMenu(true /* animated */);
182         });
183     }
184 
getHandleView()185     public BubbleBarHandleView getHandleView() {
186         return mHandleView;
187     }
188 
189     // TODO (b/275087636): call this when theme/config changes
190     /** Updates the view based on the current theme. */
applyThemeAttrs()191     public void applyThemeAttrs() {
192         boolean supportsRoundedCorners = ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
193                 mContext.getResources());
194         final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
195                 android.R.attr.dialogCornerRadius,
196                 android.R.attr.colorBackgroundFloating});
197         mCornerRadius = supportsRoundedCorners ? ta.getDimensionPixelSize(0, 0) : 0;
198         mCornerRadius = mCornerRadius / 2f;
199         mBackgroundColor = ta.getColor(1, Color.WHITE);
200 
201         ta.recycle();
202 
203         mCaptionHeight = getResources().getDimensionPixelSize(
204                 R.dimen.bubble_bar_expanded_view_caption_height);
205 
206         if (mTaskView != null) {
207             mTaskView.setCornerRadius(mCornerRadius);
208             updateHandleColor(true /* animated */);
209         }
210     }
211 
212     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)213     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
214         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
215         int height = MeasureSpec.getSize(heightMeasureSpec);
216         int menuViewHeight = Math.min(mCaptionHeight, height);
217         measureChild(mHandleView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(menuViewHeight,
218                 MeasureSpec.getMode(heightMeasureSpec)));
219 
220         if (mTaskView != null) {
221             measureChild(mTaskView, widthMeasureSpec, MeasureSpec.makeMeasureSpec(height,
222                     MeasureSpec.getMode(heightMeasureSpec)));
223         }
224     }
225 
226     @Override
onLayout(boolean changed, int l, int t, int r, int b)227     protected void onLayout(boolean changed, int l, int t, int r, int b) {
228         super.onLayout(changed, l, t, r, b);
229         final int captionBottom = t + mCaptionHeight;
230         if (mTaskView != null) {
231             mTaskView.layout(l, t, r,
232                     t + mTaskView.getMeasuredHeight());
233             mTaskView.setCaptionInsets(Insets.of(0, mCaptionHeight, 0, 0));
234         }
235         // Handle draws on top of task view in the caption area.
236         mHandleView.layout(l, t, r, captionBottom);
237     }
238 
239     @Override
onTaskCreated()240     public void onTaskCreated() {
241         setContentVisibility(true);
242         updateHandleColor(false /* animated */);
243         if (mListener != null) {
244             mListener.onTaskCreated();
245         }
246     }
247 
248     @Override
onContentVisibilityChanged(boolean visible)249     public void onContentVisibilityChanged(boolean visible) {
250         setContentVisibility(visible);
251     }
252 
253     @Override
onBackPressed()254     public void onBackPressed() {
255         if (mListener == null) return;
256         mListener.onBackPressed();
257     }
258 
259     /** Cleans up task view, should be called when the bubble is no longer active. */
cleanUpExpandedState()260     public void cleanUpExpandedState() {
261         if (mBubbleTaskViewHelper != null) {
262             if (mTaskView != null) {
263                 removeView(mTaskView);
264             }
265             mBubbleTaskViewHelper.cleanUpTaskView();
266         }
267         mMenuViewController.hideMenu(false /* animated */);
268     }
269 
270     /**
271      * Hides the current modal menu view or collapses the bubble stack.
272      * Called from {@link BubbleBarLayerView}
273      */
hideMenuOrCollapse()274     public void hideMenuOrCollapse() {
275         if (mMenuViewController.isMenuVisible()) {
276             mMenuViewController.hideMenu(/* animated = */ true);
277         } else {
278             mController.collapseStack();
279         }
280     }
281 
282     /** Updates the bubble shown in the expanded view. */
update(Bubble bubble)283     public void update(Bubble bubble) {
284         mBubbleTaskViewHelper.update(bubble);
285         mMenuViewController.updateMenu(bubble);
286     }
287 
288     /** The task id of the activity shown in the task view, if it exists. */
getTaskId()289     public int getTaskId() {
290         return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID;
291     }
292 
293     /** Sets layer bounds supplier used for obscured touchable region of task view */
setLayerBoundsSupplier(@ullable Supplier<Rect> supplier)294     void setLayerBoundsSupplier(@Nullable Supplier<Rect> supplier) {
295         mLayerBoundsSupplier = supplier;
296     }
297 
298     /** Sets expanded view listener */
setListener(@ullable Listener listener)299     void setListener(@Nullable Listener listener) {
300         mListener = listener;
301     }
302 
303     /** Sets whether the view is obscured by some modal view */
setObscured(boolean obscured)304     void setObscured(boolean obscured) {
305         if (mTaskView == null || mLayerBoundsSupplier == null) return;
306         // Updates the obscured touchable region for the task surface.
307         mTaskView.setObscuredTouchRect(obscured ? mLayerBoundsSupplier.get() : null);
308     }
309 
310     /**
311      * Call when the location or size of the view has changed to update TaskView.
312      */
updateLocation()313     public void updateLocation() {
314         if (mTaskView != null) {
315             mTaskView.onLocationChanged();
316         }
317     }
318 
319     /** Shows the expanded view for the overflow if it exists. */
maybeShowOverflow()320     void maybeShowOverflow() {
321         if (mOverflowView != null) {
322             // post this to the looper so that the view has a chance to be laid out before it can
323             // calculate row and column sizes correctly.
324             post(() -> mOverflowView.show());
325         }
326     }
327 
328     /** Sets the alpha of the task view. */
setContentVisibility(boolean visible)329     public void setContentVisibility(boolean visible) {
330         mIsContentVisible = visible;
331 
332         if (mTaskView == null) return;
333 
334         if (!mIsAnimating) {
335             mTaskView.setAlpha(visible ? 1f : 0f);
336         }
337     }
338 
339     /**
340      * Updates the handle color based on the task view status bar or background color; if those
341      * are transparent it defaults to the background color pulled from system theme attributes.
342      */
updateHandleColor(boolean animated)343     private void updateHandleColor(boolean animated) {
344         if (mTaskView == null || mTaskView.getTaskInfo() == null) return;
345         int color = mBackgroundColor;
346         ActivityManager.TaskDescription taskDescription = mTaskView.getTaskInfo().taskDescription;
347         if (taskDescription.getStatusBarColor() != Color.TRANSPARENT) {
348             color = taskDescription.getStatusBarColor();
349         } else if (taskDescription.getBackgroundColor() != Color.TRANSPARENT) {
350             color = taskDescription.getBackgroundColor();
351         }
352         final boolean isRegionDark = Color.luminance(color) <= 0.5;
353         mHandleView.updateHandleColor(isRegionDark, animated);
354     }
355 
356     /**
357      * Sets the alpha of both this view and the task view.
358      */
setTaskViewAlpha(float alpha)359     public void setTaskViewAlpha(float alpha) {
360         if (mTaskView != null) {
361             mTaskView.setAlpha(alpha);
362         }
363         setAlpha(alpha);
364     }
365 
366     /**
367      * Sets whether the surface displaying app content should sit on top. This is useful for
368      * ordering surfaces during animations. When content is drawn on top of the app (e.g. bubble
369      * being dragged out, the manage menu) this is set to false, otherwise it should be true.
370      */
setSurfaceZOrderedOnTop(boolean onTop)371     public void setSurfaceZOrderedOnTop(boolean onTop) {
372         if (mTaskView == null) {
373             return;
374         }
375         mTaskView.setZOrderedOnTop(onTop, true /* allowDynamicChange */);
376     }
377 
378     /**
379      * Sets whether the view is animating, in this case we won't change the content visibility
380      * until the animation is done.
381      */
setAnimating(boolean animating)382     public void setAnimating(boolean animating) {
383         mIsAnimating = animating;
384         // If we're done animating, apply the correct visibility.
385         if (!animating) {
386             setContentVisibility(mIsContentVisible);
387         }
388     }
389 }
390