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.annotation.NonNull;
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.drawable.Icon;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewGroup;
26 
27 import androidx.core.content.ContextCompat;
28 import androidx.dynamicanimation.animation.DynamicAnimation;
29 import androidx.dynamicanimation.animation.SpringForce;
30 
31 import com.android.wm.shell.R;
32 import com.android.wm.shell.bubbles.Bubble;
33 import com.android.wm.shell.shared.animation.PhysicsAnimator;
34 
35 import java.util.ArrayList;
36 
37 /**
38  * Manages bubble bar expanded view menu presentation and animations
39  */
40 class BubbleBarMenuViewController {
41     private static final float MENU_INITIAL_SCALE = 0.5f;
42     private final Context mContext;
43     private final ViewGroup mRootView;
44     private @Nullable Listener mListener;
45     private @Nullable Bubble mBubble;
46     private @Nullable BubbleBarMenuView mMenuView;
47     /** A transparent view used to intercept touches to collapse menu when presented */
48     private @Nullable View mScrimView;
49     private @Nullable PhysicsAnimator<BubbleBarMenuView> mMenuAnimator;
50     private PhysicsAnimator.SpringConfig mMenuSpringConfig;
51 
BubbleBarMenuViewController(Context context, ViewGroup rootView)52     BubbleBarMenuViewController(Context context, ViewGroup rootView) {
53         mContext = context;
54         mRootView = rootView;
55         mMenuSpringConfig = new PhysicsAnimator.SpringConfig(
56                 SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
57     }
58 
59     /** Tells if the menu is visible or being animated */
isMenuVisible()60     boolean isMenuVisible() {
61         return mMenuView != null && mMenuView.getVisibility() == View.VISIBLE;
62     }
63 
64     /** Sets menu actions listener */
setListener(@ullable Listener listener)65     void setListener(@Nullable Listener listener) {
66         mListener = listener;
67     }
68 
69     /** Update menu with bubble */
updateMenu(@onNull Bubble bubble)70     void updateMenu(@NonNull Bubble bubble) {
71         mBubble = bubble;
72     }
73 
74     /**
75      * Show bubble bar expanded view menu
76      * @param animated if should animate transition
77      */
showMenu(boolean animated)78     void showMenu(boolean animated) {
79         if (mMenuView == null || mScrimView == null) {
80             setupMenu();
81         }
82         cancelAnimations();
83         mMenuView.setVisibility(View.VISIBLE);
84         mScrimView.setVisibility(View.VISIBLE);
85         Runnable endActions = () -> {
86             mMenuView.getChildAt(0).requestAccessibilityFocus();
87             if (mListener != null) {
88                 mListener.onMenuVisibilityChanged(true /* isShown */);
89             }
90         };
91         if (animated) {
92             animateTransition(true /* show */, endActions);
93         } else {
94             endActions.run();
95         }
96     }
97 
98     /**
99      * Hide bubble bar expanded view menu
100      * @param animated if should animate transition
101      */
hideMenu(boolean animated)102     void hideMenu(boolean animated) {
103         if (mMenuView == null || mScrimView == null) return;
104         cancelAnimations();
105         Runnable endActions = () -> {
106             mMenuView.setVisibility(View.GONE);
107             mScrimView.setVisibility(View.GONE);
108             if (mListener != null) {
109                 mListener.onMenuVisibilityChanged(false /* isShown */);
110             }
111         };
112         if (animated) {
113             animateTransition(false /* show */, endActions);
114         } else {
115             endActions.run();
116         }
117     }
118 
119     /**
120      * Animate show/hide menu transition
121      * @param show if should show or hide the menu
122      * @param endActions will be called when animation ends
123      */
animateTransition(boolean show, Runnable endActions)124     private void animateTransition(boolean show, Runnable endActions) {
125         if (mMenuView == null) return;
126         mMenuAnimator = PhysicsAnimator.getInstance(mMenuView);
127         mMenuAnimator.setDefaultSpringConfig(mMenuSpringConfig);
128         mMenuAnimator
129                 .spring(DynamicAnimation.ALPHA, show ? 1f : 0f)
130                 .spring(DynamicAnimation.SCALE_Y, show ? 1f : MENU_INITIAL_SCALE)
131                 .withEndActions(() -> {
132                     mMenuAnimator = null;
133                     endActions.run();
134                 })
135                 .start();
136     }
137 
138     /** Cancel running animations */
cancelAnimations()139     private void cancelAnimations() {
140         if (mMenuAnimator != null) {
141             mMenuAnimator.cancel();
142             mMenuAnimator = null;
143         }
144     }
145 
146     /** Sets up and inflate menu views */
setupMenu()147     private void setupMenu() {
148         // Menu view setup
149         mMenuView = (BubbleBarMenuView) LayoutInflater.from(mContext).inflate(
150                 R.layout.bubble_bar_menu_view, mRootView, false);
151         mMenuView.setAlpha(0f);
152         mMenuView.setPivotY(0f);
153         mMenuView.setScaleY(MENU_INITIAL_SCALE);
154         mMenuView.setOnCloseListener(() -> hideMenu(true  /* animated */));
155         if (mBubble != null) {
156             mMenuView.updateInfo(mBubble);
157             mMenuView.updateActions(createMenuActions(mBubble));
158         }
159         // Scrim view setup
160         mScrimView = new View(mContext);
161         mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
162         mScrimView.setOnClickListener(view -> hideMenu(true  /* animated */));
163         // Attach to root view
164         mRootView.addView(mScrimView);
165         mRootView.addView(mMenuView);
166     }
167 
168     /**
169      * Creates menu actions to populate menu view
170      * @param bubble used to create actions depending on bubble type
171      */
createMenuActions(Bubble bubble)172     private ArrayList<BubbleBarMenuView.MenuAction> createMenuActions(Bubble bubble) {
173         ArrayList<BubbleBarMenuView.MenuAction> menuActions = new ArrayList<>();
174         Resources resources = mContext.getResources();
175 
176         if (bubble.isConversation()) {
177             // Don't bubble conversation action
178             menuActions.add(new BubbleBarMenuView.MenuAction(
179                     Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble),
180                     resources.getString(R.string.bubbles_dont_bubble_conversation),
181                     view -> {
182                         hideMenu(true /* animated */);
183                         if (mListener != null) {
184                             mListener.onUnBubbleConversation(bubble);
185                         }
186                     }
187             ));
188             // Open settings action
189             Icon appIcon = bubble.getRawAppBadge() != null ? Icon.createWithBitmap(
190                     bubble.getRawAppBadge()) : null;
191             menuActions.add(new BubbleBarMenuView.MenuAction(
192                     appIcon,
193                     resources.getString(R.string.bubbles_app_settings, bubble.getAppName()),
194                     view -> {
195                         hideMenu(true /* animated */);
196                         if (mListener != null) {
197                             mListener.onOpenAppSettings(bubble);
198                         }
199                     }
200             ));
201         }
202 
203         // Dismiss bubble action
204         menuActions.add(new BubbleBarMenuView.MenuAction(
205                 Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow),
206                 resources.getString(R.string.bubble_dismiss_text),
207                 ContextCompat.getColor(mContext, R.color.bubble_bar_expanded_view_menu_close),
208                 view -> {
209                     hideMenu(true /* animated */);
210                     if (mListener != null) {
211                         mListener.onDismissBubble(bubble);
212                     }
213                 }
214         ));
215 
216         return menuActions;
217     }
218 
219     /**
220      * Bubble bar expanded view menu actions listener
221      */
222     interface Listener {
223         /**
224          * Called when manage menu is shown/hidden
225          * If animated will be called when animation ends
226          */
onMenuVisibilityChanged(boolean visible)227         void onMenuVisibilityChanged(boolean visible);
228 
229         /**
230          * Un-bubbles conversation and removes the bubble from the stack
231          * This conversation will not be bubbled with new messages
232          * @see com.android.wm.shell.bubbles.BubbleController
233          */
onUnBubbleConversation(Bubble bubble)234         void onUnBubbleConversation(Bubble bubble);
235 
236         /**
237          * Launches app notification bubble settings for the bubble with intent created in:
238          * {@code Bubble.getSettingsIntent}
239          */
onOpenAppSettings(Bubble bubble)240         void onOpenAppSettings(Bubble bubble);
241 
242         /**
243          * Dismiss bubble and remove it from the bubble stack
244          */
onDismissBubble(Bubble bubble)245         void onDismissBubble(Bubble bubble);
246     }
247 }
248