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