1 /* 2 * Copyright (C) 2022 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.systemui.dreams.touch; 18 19 import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING; 20 import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING; 21 import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.ValueAnimator; 26 import android.graphics.Rect; 27 import android.graphics.Region; 28 import android.util.Log; 29 import android.view.GestureDetector; 30 import android.view.InputEvent; 31 import android.view.MotionEvent; 32 import android.view.VelocityTracker; 33 34 import androidx.annotation.VisibleForTesting; 35 36 import com.android.internal.logging.UiEvent; 37 import com.android.internal.logging.UiEventLogger; 38 import com.android.systemui.dreams.touch.scrim.ScrimController; 39 import com.android.systemui.dreams.touch.scrim.ScrimManager; 40 import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants; 41 import com.android.systemui.shade.ShadeExpansionChangeEvent; 42 import com.android.systemui.statusbar.NotificationShadeWindowController; 43 import com.android.systemui.statusbar.phone.CentralSurfaces; 44 import com.android.wm.shell.animation.FlingAnimationUtils; 45 46 import java.util.Optional; 47 48 import javax.inject.Inject; 49 import javax.inject.Named; 50 51 /** 52 * Monitor for tracking touches on the DreamOverlay to bring up the bouncer. 53 */ 54 public class BouncerSwipeTouchHandler implements DreamTouchHandler { 55 /** 56 * An interface for creating ValueAnimators. 57 */ 58 public interface ValueAnimatorCreator { 59 /** 60 * Creates {@link ValueAnimator}. 61 */ create(float start, float finish)62 ValueAnimator create(float start, float finish); 63 } 64 65 /** 66 * An interface for obtaining VelocityTrackers. 67 */ 68 public interface VelocityTrackerFactory { 69 /** 70 * Obtains {@link VelocityTracker}. 71 */ obtain()72 VelocityTracker obtain(); 73 } 74 75 public static final float FLING_PERCENTAGE_THRESHOLD = 0.5f; 76 77 private static final String TAG = "BouncerSwipeTouchHandler"; 78 private final NotificationShadeWindowController mNotificationShadeWindowController; 79 private final float mBouncerZoneScreenPercentage; 80 81 private final ScrimManager mScrimManager; 82 private ScrimController mCurrentScrimController; 83 private float mCurrentExpansion; 84 private final Optional<CentralSurfaces> mCentralSurfaces; 85 86 private VelocityTracker mVelocityTracker; 87 88 private final FlingAnimationUtils mFlingAnimationUtils; 89 private final FlingAnimationUtils mFlingAnimationUtilsClosing; 90 91 private Boolean mCapture; 92 private Boolean mExpanded; 93 94 private boolean mBouncerInitiallyShowing; 95 96 private TouchSession mTouchSession; 97 98 private ValueAnimatorCreator mValueAnimatorCreator; 99 100 private VelocityTrackerFactory mVelocityTrackerFactory; 101 102 private final UiEventLogger mUiEventLogger; 103 104 private final ScrimManager.Callback mScrimManagerCallback = new ScrimManager.Callback() { 105 @Override 106 public void onScrimControllerChanged(ScrimController controller) { 107 if (mCurrentScrimController != null) { 108 mCurrentScrimController.reset(); 109 } 110 111 mCurrentScrimController = controller; 112 } 113 }; 114 115 private final GestureDetector.OnGestureListener mOnGestureListener = 116 new GestureDetector.SimpleOnGestureListener() { 117 @Override 118 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, 119 float distanceY) { 120 if (mCapture == null) { 121 // If the user scrolling favors a vertical direction, begin capturing 122 // scrolls. 123 mCapture = Math.abs(distanceY) > Math.abs(distanceX); 124 mBouncerInitiallyShowing = mCentralSurfaces 125 .map(CentralSurfaces::isBouncerShowing) 126 .orElse(false); 127 128 if (mCapture) { 129 // reset expanding 130 mExpanded = false; 131 // Since the user is dragging the bouncer up, set scrimmed to false. 132 mCurrentScrimController.show(); 133 } 134 } 135 136 if (!mCapture) { 137 return false; 138 } 139 140 // Don't set expansion for downward scroll when the bouncer is hidden. 141 if (!mBouncerInitiallyShowing && (e1.getY() < e2.getY())) { 142 return true; 143 } 144 145 // Don't set expansion for upward scroll when the bouncer is shown. 146 if (mBouncerInitiallyShowing && (e1.getY() > e2.getY())) { 147 return true; 148 } 149 150 if (!mCentralSurfaces.isPresent()) { 151 return true; 152 } 153 154 // For consistency, we adopt the expansion definition found in the 155 // PanelViewController. In this case, expansion refers to the view above the 156 // bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer 157 // is fully hidden at full expansion (1) and fully visible when fully collapsed 158 // (0). 159 final float dragDownAmount = e2.getY() - e1.getY(); 160 final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY()) 161 / mTouchSession.getBounds().height(); 162 setPanelExpansion(mBouncerInitiallyShowing 163 ? screenTravelPercentage : 1 - screenTravelPercentage, dragDownAmount); 164 return true; 165 } 166 }; 167 setPanelExpansion(float expansion, float dragDownAmount)168 private void setPanelExpansion(float expansion, float dragDownAmount) { 169 mCurrentExpansion = expansion; 170 ShadeExpansionChangeEvent event = 171 new ShadeExpansionChangeEvent( 172 /* fraction= */ mCurrentExpansion, 173 /* expanded= */ mExpanded, 174 /* tracking= */ true, 175 /* dragDownPxAmount= */ dragDownAmount); 176 mCurrentScrimController.expand(event); 177 } 178 179 180 @VisibleForTesting 181 public enum DreamEvent implements UiEventLogger.UiEventEnum { 182 @UiEvent(doc = "The screensaver has been swiped up.") 183 DREAM_SWIPED(988), 184 185 @UiEvent(doc = "The bouncer has become fully visible over dream.") 186 DREAM_BOUNCER_FULLY_VISIBLE(1056); 187 188 private final int mId; 189 DreamEvent(int id)190 DreamEvent(int id) { 191 mId = id; 192 } 193 194 @Override getId()195 public int getId() { 196 return mId; 197 } 198 } 199 200 @Inject BouncerSwipeTouchHandler( ScrimManager scrimManager, Optional<CentralSurfaces> centralSurfaces, NotificationShadeWindowController notificationShadeWindowController, ValueAnimatorCreator valueAnimatorCreator, VelocityTrackerFactory velocityTrackerFactory, @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING) FlingAnimationUtils flingAnimationUtils, @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING) FlingAnimationUtils flingAnimationUtilsClosing, @Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage, UiEventLogger uiEventLogger)201 public BouncerSwipeTouchHandler( 202 ScrimManager scrimManager, 203 Optional<CentralSurfaces> centralSurfaces, 204 NotificationShadeWindowController notificationShadeWindowController, 205 ValueAnimatorCreator valueAnimatorCreator, 206 VelocityTrackerFactory velocityTrackerFactory, 207 @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING) 208 FlingAnimationUtils flingAnimationUtils, 209 @Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING) 210 FlingAnimationUtils flingAnimationUtilsClosing, 211 @Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage, 212 UiEventLogger uiEventLogger) { 213 mCentralSurfaces = centralSurfaces; 214 mScrimManager = scrimManager; 215 mNotificationShadeWindowController = notificationShadeWindowController; 216 mBouncerZoneScreenPercentage = swipeRegionPercentage; 217 mFlingAnimationUtils = flingAnimationUtils; 218 mFlingAnimationUtilsClosing = flingAnimationUtilsClosing; 219 mValueAnimatorCreator = valueAnimatorCreator; 220 mVelocityTrackerFactory = velocityTrackerFactory; 221 mUiEventLogger = uiEventLogger; 222 } 223 224 @Override getTouchInitiationRegion(Rect bounds, Region region)225 public void getTouchInitiationRegion(Rect bounds, Region region) { 226 final int width = bounds.width(); 227 final int height = bounds.height(); 228 229 if (mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) { 230 region.op(new Rect(0, 0, width, 231 Math.round( 232 height * mBouncerZoneScreenPercentage)), 233 Region.Op.UNION); 234 } else { 235 region.op(new Rect(0, 236 Math.round(height * (1 - mBouncerZoneScreenPercentage)), 237 width, 238 height), 239 Region.Op.UNION); 240 } 241 } 242 243 @Override onSessionStart(TouchSession session)244 public void onSessionStart(TouchSession session) { 245 mVelocityTracker = mVelocityTrackerFactory.obtain(); 246 mTouchSession = session; 247 mVelocityTracker.clear(); 248 mNotificationShadeWindowController.setForcePluginOpen(true, this); 249 mScrimManager.addCallback(mScrimManagerCallback); 250 mCurrentScrimController = mScrimManager.getCurrentController(); 251 252 session.registerCallback(() -> { 253 if (mVelocityTracker != null) { 254 mVelocityTracker.recycle(); 255 mVelocityTracker = null; 256 } 257 mScrimManager.removeCallback(mScrimManagerCallback); 258 mCapture = null; 259 mNotificationShadeWindowController.setForcePluginOpen(false, this); 260 }); 261 262 session.registerGestureListener(mOnGestureListener); 263 session.registerInputListener(ev -> onMotionEvent(ev)); 264 265 } 266 onMotionEvent(InputEvent event)267 private void onMotionEvent(InputEvent event) { 268 if (!(event instanceof MotionEvent)) { 269 Log.e(TAG, "non MotionEvent received:" + event); 270 return; 271 } 272 273 final MotionEvent motionEvent = (MotionEvent) event; 274 275 switch (motionEvent.getAction()) { 276 case MotionEvent.ACTION_CANCEL: 277 case MotionEvent.ACTION_UP: 278 mTouchSession.pop(); 279 // If we are not capturing any input, there is no need to consider animating to 280 // finish transition. 281 if (mCapture == null || !mCapture) { 282 break; 283 } 284 285 // We must capture the resulting velocities as resetMonitor() will clear these 286 // values. 287 mVelocityTracker.computeCurrentVelocity(1000); 288 final float verticalVelocity = mVelocityTracker.getYVelocity(); 289 final float horizontalVelocity = mVelocityTracker.getXVelocity(); 290 291 final float velocityVector = 292 (float) Math.hypot(horizontalVelocity, verticalVelocity); 293 294 mExpanded = !flingRevealsOverlay(verticalVelocity, velocityVector); 295 final float expansion = mExpanded 296 ? KeyguardBouncerConstants.EXPANSION_VISIBLE 297 : KeyguardBouncerConstants.EXPANSION_HIDDEN; 298 299 // Log the swiping up to show Bouncer event. 300 if (!mBouncerInitiallyShowing 301 && expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { 302 mUiEventLogger.log(DreamEvent.DREAM_SWIPED); 303 } 304 305 flingToExpansion(verticalVelocity, expansion); 306 307 if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) { 308 mCurrentScrimController.reset(); 309 } 310 break; 311 default: 312 mVelocityTracker.addMovement(motionEvent); 313 break; 314 } 315 } 316 createExpansionAnimator(float targetExpansion, float expansionHeight)317 private ValueAnimator createExpansionAnimator(float targetExpansion, float expansionHeight) { 318 final ValueAnimator animator = 319 mValueAnimatorCreator.create(mCurrentExpansion, targetExpansion); 320 animator.addUpdateListener( 321 animation -> { 322 float expansionFraction = (float) animation.getAnimatedValue(); 323 float dragDownAmount = expansionFraction * expansionHeight; 324 setPanelExpansion(expansionFraction, dragDownAmount); 325 }); 326 if (!mBouncerInitiallyShowing 327 && targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) { 328 animator.addListener( 329 new AnimatorListenerAdapter() { 330 @Override 331 public void onAnimationEnd(Animator animation) { 332 mUiEventLogger.log(DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE); 333 } 334 }); 335 } 336 return animator; 337 } 338 flingRevealsOverlay(float velocity, float velocityVector)339 protected boolean flingRevealsOverlay(float velocity, float velocityVector) { 340 // Fully expand the space above the bouncer, if the user has expanded the bouncer less 341 // than halfway or final velocity was positive, indicating a downward direction. 342 if (Math.abs(velocityVector) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 343 return mCurrentExpansion > FLING_PERCENTAGE_THRESHOLD; 344 } else { 345 return velocity > 0; 346 } 347 } 348 flingToExpansion(float velocity, float expansion)349 protected void flingToExpansion(float velocity, float expansion) { 350 if (!mCentralSurfaces.isPresent()) { 351 return; 352 } 353 354 // The animation utils deal in pixel units, rather than expansion height. 355 final float viewHeight = mTouchSession.getBounds().height(); 356 final float currentHeight = viewHeight * mCurrentExpansion; 357 final float targetHeight = viewHeight * expansion; 358 final float expansionHeight = targetHeight - currentHeight; 359 final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight); 360 if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) { 361 // Hides the bouncer, i.e., fully expands the space above the bouncer. 362 mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity, 363 viewHeight); 364 } else { 365 // Shows the bouncer, i.e., fully collapses the space above the bouncer. 366 mFlingAnimationUtils.apply( 367 animator, currentHeight, targetHeight, velocity, viewHeight); 368 } 369 370 animator.start(); 371 } 372 } 373