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