• 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 package com.android.wm.shell.bubbles.bar;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.ValueAnimator;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.drawable.Icon;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import com.android.app.animation.Interpolators;
31 import com.android.wm.shell.R;
32 import com.android.wm.shell.bubbles.Bubble;
33 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
34 
35 import java.util.ArrayList;
36 
37 /**
38  * Manages bubble bar expanded view menu presentation and animations
39  */
40 class BubbleBarMenuViewController {
41 
42     private static final float WIDTH_SWAP_FRACTION = 0.4F;
43     private static final long MENU_ANIMATION_DURATION = 600;
44 
45     private final Context mContext;
46     private final ViewGroup mRootView;
47     private final BubbleBarHandleView mHandleView;
48     private @Nullable Listener mListener;
49     private @Nullable Bubble mBubble;
50     private @Nullable BubbleBarMenuView mMenuView;
51     /** A transparent view used to intercept touches to collapse menu when presented */
52     private @Nullable View mScrimView;
53     private @Nullable ValueAnimator mMenuAnimator;
54 
55 
BubbleBarMenuViewController(Context context, BubbleBarHandleView handleView, ViewGroup rootView)56     BubbleBarMenuViewController(Context context, BubbleBarHandleView handleView,
57             ViewGroup rootView) {
58         mContext = context;
59         mRootView = rootView;
60         mHandleView = handleView;
61     }
62 
63     /** Tells if the menu is visible or being animated */
isMenuVisible()64     boolean isMenuVisible() {
65         return mMenuView != null && mMenuView.getVisibility() == View.VISIBLE;
66     }
67 
68     /** Sets menu actions listener */
setListener(@ullable Listener listener)69     void setListener(@Nullable Listener listener) {
70         mListener = listener;
71     }
72 
73     /** Update menu with bubble */
updateMenu(@onNull Bubble bubble)74     void updateMenu(@NonNull Bubble bubble) {
75         mBubble = bubble;
76     }
77 
78     /**
79      * Show bubble bar expanded view menu
80      * @param animated if should animate transition
81      */
showMenu(boolean animated)82     void showMenu(boolean animated) {
83         if (mMenuView == null || mScrimView == null) {
84             setupMenu();
85         }
86         runOnMenuIsMeasured(() -> {
87             mMenuView.setVisibility(View.VISIBLE);
88             mScrimView.setVisibility(View.VISIBLE);
89             Runnable endActions = () -> {
90                 mMenuView.getChildAt(0).requestAccessibilityFocus();
91                 if (mListener != null) {
92                     mListener.onMenuVisibilityChanged(true /* isShown */);
93                 }
94             };
95             if (animated) {
96                 animateTransition(true /* show */, endActions);
97             } else {
98                 endActions.run();
99             }
100         });
101     }
102 
103     /**
104      * Hide bubble bar expanded view menu
105      * @param animated if should animate transition
106      */
hideMenu(boolean animated)107     void hideMenu(boolean animated) {
108         if (mMenuView == null || mScrimView == null) return;
109         runOnMenuIsMeasured(() -> {
110             Runnable endActions = () -> {
111                 mHandleView.restoreAnimationDefaults();
112                 mMenuView.setVisibility(View.GONE);
113                 mScrimView.setVisibility(View.GONE);
114                 mHandleView.setVisibility(View.VISIBLE);
115                 if (mListener != null) {
116                     mListener.onMenuVisibilityChanged(false /* isShown */);
117                 }
118             };
119             if (animated) {
120                 animateTransition(false /* show */, endActions);
121             } else {
122                 endActions.run();
123             }
124         });
125     }
126 
runOnMenuIsMeasured(Runnable action)127     private void runOnMenuIsMeasured(Runnable action) {
128         if (mMenuView.getWidth() == 0 || mMenuView.getHeight() == 0) {
129             // the menu view is not yet measured, postpone showing the animation
130             mMenuView.post(() -> runOnMenuIsMeasured(action));
131         } else {
132             action.run();
133         }
134     }
135 
136     /**
137      * Animate show/hide menu transition
138      * @param show if should show or hide the menu
139      * @param endActions will be called when animation ends
140      */
animateTransition(boolean show, Runnable endActions)141     private void animateTransition(boolean show, Runnable endActions) {
142         if (mMenuView == null) return;
143         float startValue = show ? 0 : 1;
144         if (mMenuAnimator != null && mMenuAnimator.isRunning()) {
145             startValue = (float) mMenuAnimator.getAnimatedValue();
146             mMenuAnimator.cancel();
147         }
148         ValueAnimator showMenuAnimation = ValueAnimator.ofFloat(startValue, show ? 1 : 0);
149         showMenuAnimation.setDuration(MENU_ANIMATION_DURATION);
150         showMenuAnimation.setInterpolator(Interpolators.EMPHASIZED);
151         showMenuAnimation.addListener(new AnimatorListenerAdapter() {
152             @Override
153             public void onAnimationEnd(Animator animation) {
154                 mMenuAnimator = null;
155                 endActions.run();
156             }
157         });
158         mMenuAnimator = showMenuAnimation;
159         setupAnimatorListener(showMenuAnimation);
160         showMenuAnimation.start();
161     }
162 
163     /** Setup listener that orchestrates the animation. */
setupAnimatorListener(ValueAnimator showMenuAnimation)164     private void setupAnimatorListener(ValueAnimator showMenuAnimation) {
165         // Getting views properties start values
166         int widthDiff = mMenuView.getWidth() - mHandleView.getHandleWidth();
167         int handleHeight = mHandleView.getHandleHeight();
168         float targetWidth = mHandleView.getHandleWidth() + widthDiff * WIDTH_SWAP_FRACTION;
169         float targetHeight = targetWidth * mMenuView.getTitleItemHeight() / mMenuView.getWidth();
170         int menuColor = mContext.getColor(com.android.internal.R.color.materialColorSurfaceBright);
171         // Calculating deltas
172         float swapScale = targetWidth / mMenuView.getWidth();
173         float handleWidthDelta = targetWidth - mHandleView.getHandleWidth();
174         float handleHeightDelta = targetHeight - handleHeight;
175         // Setting update listener that will orchestrate the animation
176         showMenuAnimation.addUpdateListener(animator -> {
177             float animationProgress = (float) animator.getAnimatedValue();
178             boolean showHandle = animationProgress <= WIDTH_SWAP_FRACTION;
179             mHandleView.setVisibility(showHandle ? View.VISIBLE : View.GONE);
180             mMenuView.setVisibility(showHandle ? View.GONE : View.VISIBLE);
181             if (showHandle) {
182                 float handleAnimationProgress = animationProgress / WIDTH_SWAP_FRACTION;
183                 mHandleView.animateHandleForMenu(handleAnimationProgress, handleWidthDelta,
184                         handleHeightDelta, menuColor);
185             } else {
186                 mMenuView.setTranslationY(mHandleView.getHandlePaddingTop());
187                 mMenuView.setPivotY(0);
188                 mMenuView.setPivotX((float) mMenuView.getWidth() / 2);
189                 float menuAnimationProgress =
190                         (animationProgress - WIDTH_SWAP_FRACTION) / (1 - WIDTH_SWAP_FRACTION);
191                 float currentMenuScale = swapScale + (1 - swapScale) * menuAnimationProgress;
192                 mMenuView.animateFromStartScale(currentMenuScale, menuAnimationProgress);
193             }
194         });
195     }
196 
197     /** Sets up and inflate menu views */
setupMenu()198     private void setupMenu() {
199         // Menu view setup
200         mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
201                 R.layout.bubble_bar_menu_view, mRootView, false);
202         mMenuView.setOnCloseListener(() -> hideMenu(true  /* animated */));
203         if (mBubble != null) {
204             mMenuView.updateInfo(mBubble);
205             mMenuView.updateActions(createMenuActions(mBubble));
206         }
207         // Scrim view setup
208         mScrimView = new View(mContext);
209         mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
210         mScrimView.setOnClickListener(view -> hideMenu(true  /* animated */));
211         // Attach to root view
212         mRootView.addView(mScrimView);
213         mRootView.addView(mMenuView);
214     }
215 
216     /**
217      * Creates menu actions to populate menu view
218      * @param bubble used to create actions depending on bubble type
219      */
createMenuActions(Bubble bubble)220     private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) {
221         ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>();
222         Resources resources = mContext.getResources();
223         int tintColor = mContext.getColor(com.android.internal.R.color.materialColorOnSurface);
224 
225         if (bubble.isChat()) {
226             // Don't bubble conversation action
227             menuActions.add(new BubbleBarMenuView.MenuAction(
228                     Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble),
229                     resources.getString(R.string.bubbles_dont_bubble_conversation),
230                     tintColor,
231                     view -> {
232                         hideMenu(true /* animated */);
233                         if (mListener != null) {
234                             mListener.onUnBubbleConversation(bubble);
235                         }
236                     }
237             ));
238             // Open settings action
239             Icon appIcon = bubble.getRawAppBadge() != null ? Icon.createWithBitmap(
240                     bubble.getRawAppBadge()) : null;
241             menuActions.add(new BubbleBarMenuView.MenuAction(
242                     appIcon,
243                     resources.getString(R.string.bubbles_app_settings, bubble.getAppName()),
244                     view -> {
245                         hideMenu(true /* animated */);
246                         if (mListener != null) {
247                             mListener.onOpenAppSettings(bubble);
248                         }
249                     }
250             ));
251         }
252 
253         // Dismiss bubble action
254         menuActions.add(new BubbleBarMenuView.MenuAction(
255                 Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
256                 resources.getString(R.string.bubble_dismiss_text),
257                 tintColor,
258                 view -> {
259                     hideMenu(true /* animated */);
260                     if (mListener != null) {
261                         mListener.onDismissBubble(bubble);
262                     }
263                 }
264         ));
265 
266         if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
267             menuActions.add(new BubbleBarMenuView.MenuAction(
268                     Icon.createWithResource(resources,
269                             R.drawable.desktop_mode_ic_handle_menu_fullscreen),
270                     resources.getString(R.string.bubble_fullscreen_text),
271                     tintColor,
272                     view -> {
273                         hideMenu(true /* animated */);
274                         if (mListener != null) {
275                             mListener.onMoveToFullscreen(bubble);
276                         }
277                     }
278             ));
279         }
280 
281         return menuActions;
282     }
283 
284     /**
285      * Bubble bar expanded view menu actions listener
286      */
287     interface Listener {
288         /**
289          * Called when manage menu is shown/hidden
290          * If animated will be called when animation ends
291          */
onMenuVisibilityChanged(boolean visible)292         void onMenuVisibilityChanged(boolean visible);
293 
294         /**
295          * Un-bubbles conversation and removes the bubble from the stack
296          * This conversation will not be bubbled with new messages
297          * @see com.android.wm.shell.bubbles.BubbleController
298          */
onUnBubbleConversation(Bubble bubble)299         void onUnBubbleConversation(Bubble bubble);
300 
301         /**
302          * Launches app notification bubble settings for the bubble with intent created in:
303          * {@code Bubble.getSettingsIntent}
304          */
onOpenAppSettings(Bubble bubble)305         void onOpenAppSettings(Bubble bubble);
306 
307         /**
308          * Dismiss bubble and remove it from the bubble stack
309          */
onDismissBubble(Bubble bubble)310         void onDismissBubble(Bubble bubble);
311 
312         /**
313          * Move the bubble to fullscreen.
314          */
onMoveToFullscreen(Bubble bubble)315         void onMoveToFullscreen(Bubble bubble);
316     }
317 }
318