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