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