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