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