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