1 /*
2  * Copyright (C) 2018 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 Licen
15  */
16 
17 
18 package com.android.systemui.statusbar.notification.stack;
19 
20 import android.animation.Animator;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.graphics.Rect;
24 import android.os.Handler;
25 import android.service.notification.StatusBarNotification;
26 import android.view.MotionEvent;
27 import android.view.View;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.systemui.SwipeHelper;
31 import com.android.systemui.plugins.FalsingManager;
32 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
33 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
34 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
35 import com.android.systemui.statusbar.notification.row.ExpandableView;
36 
37 class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeActionHelper {
38 
39     @VisibleForTesting
40     protected static final long COVER_MENU_DELAY = 4000;
41     private static final String TAG = "NotificationSwipeHelper";
42     private final Runnable mFalsingCheck;
43     private View mTranslatingParentView;
44     private View mMenuExposedView;
45     private final NotificationCallback mCallback;
46     private final NotificationMenuRowPlugin.OnMenuEventListener mMenuListener;
47 
48     private static final long SWIPE_MENU_TIMING = 200;
49 
50     private NotificationMenuRowPlugin mCurrMenuRow;
51     private boolean mIsExpanded;
52     private boolean mPulsing;
53 
NotificationSwipeHelper( int swipeDirection, NotificationCallback callback, Context context, NotificationMenuRowPlugin.OnMenuEventListener menuListener, FalsingManager falsingManager)54     NotificationSwipeHelper(
55             int swipeDirection, NotificationCallback callback, Context context,
56             NotificationMenuRowPlugin.OnMenuEventListener menuListener,
57             FalsingManager falsingManager) {
58         super(swipeDirection, callback, context, falsingManager);
59         mMenuListener = menuListener;
60         mCallback = callback;
61         mFalsingCheck = () -> resetExposedMenuView(true /* animate */, true /* force */);
62     }
63 
getTranslatingParentView()64     public View getTranslatingParentView() {
65         return mTranslatingParentView;
66     }
67 
clearTranslatingParentView()68     public void clearTranslatingParentView() { setTranslatingParentView(null); }
69 
70     @VisibleForTesting
setTranslatingParentView(View view)71     protected void setTranslatingParentView(View view) { mTranslatingParentView = view; };
72 
setExposedMenuView(View view)73     public void setExposedMenuView(View view) {
74         mMenuExposedView = view;
75     }
76 
clearExposedMenuView()77     public void clearExposedMenuView() { setExposedMenuView(null); }
78 
clearCurrentMenuRow()79     public void clearCurrentMenuRow() { setCurrentMenuRow(null); }
80 
getExposedMenuView()81     public View getExposedMenuView() {
82         return mMenuExposedView;
83     }
84 
setCurrentMenuRow(NotificationMenuRowPlugin menuRow)85     public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) {
86         mCurrMenuRow = menuRow;
87     }
88 
getCurrentMenuRow()89     public NotificationMenuRowPlugin getCurrentMenuRow() {  return mCurrMenuRow; }
90 
91     @VisibleForTesting
getHandler()92     protected Handler getHandler() { return mHandler; }
93 
94     @VisibleForTesting
getFalsingCheck()95     protected Runnable getFalsingCheck() {
96         return mFalsingCheck;
97     }
98 
setIsExpanded(boolean isExpanded)99     public void setIsExpanded(boolean isExpanded) {
100         mIsExpanded = isExpanded;
101     }
102 
103     @Override
onChildSnappedBack(View animView, float targetLeft)104     protected void onChildSnappedBack(View animView, float targetLeft) {
105         if (mCurrMenuRow != null && targetLeft == 0) {
106             mCurrMenuRow.resetMenu();
107             clearCurrentMenuRow();
108         }
109     }
110 
111     @Override
onDownUpdate(View currView, MotionEvent ev)112     public void onDownUpdate(View currView, MotionEvent ev) {
113         mTranslatingParentView = currView;
114         NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
115         if (menuRow != null) {
116             menuRow.onTouchStart();
117         }
118         clearCurrentMenuRow();
119         getHandler().removeCallbacks(getFalsingCheck());
120 
121         // Slide back any notifications that might be showing a menu
122         resetExposedMenuView(true /* animate */, false /* force */);
123 
124         if (currView instanceof SwipeableView) {
125             initializeRow((SwipeableView) currView);
126         }
127     }
128 
129     @VisibleForTesting
initializeRow(SwipeableView row)130     protected void initializeRow(SwipeableView row) {
131         if (row.hasFinishedInitialization()) {
132             mCurrMenuRow = row.createMenu();
133             if (mCurrMenuRow != null) {
134                 mCurrMenuRow.setMenuClickListener(mMenuListener);
135                 mCurrMenuRow.onTouchStart();
136             }
137         }
138     }
139 
swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow)140     private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
141         return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
142     }
143 
144     @Override
onMoveUpdate(View view, MotionEvent ev, float translation, float delta)145     public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
146         getHandler().removeCallbacks(getFalsingCheck());
147         NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
148         if (menuRow != null) {
149             menuRow.onTouchMove(delta);
150         }
151     }
152 
153     @Override
handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)154     public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
155             float translation) {
156         NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
157         if (menuRow != null) {
158             menuRow.onTouchEnd();
159             handleMenuRowSwipe(ev, animView, velocity, menuRow);
160             return true;
161         }
162         return false;
163     }
164 
165     @VisibleForTesting
handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)166     protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity,
167             NotificationMenuRowPlugin menuRow) {
168         if (!menuRow.shouldShowMenu()) {
169             // If the menu should not be shown, then there is no need to check if the a swipe
170             // should result in a snapping to the menu. As a result, just check if the swipe
171             // was enough to dismiss the notification.
172             if (isDismissGesture(ev)) {
173                 dismiss(animView, velocity);
174             } else {
175                 snapClosed(animView, velocity);
176                 menuRow.onSnapClosed();
177             }
178             return;
179         }
180 
181         if (menuRow.isSnappedAndOnSameSide()) {
182             // Menu was snapped to previously and we're on the same side
183             handleSwipeFromOpenState(ev, animView, velocity, menuRow);
184         } else {
185             // Menu has not been snapped, or was snapped previously but is now on
186             // the opposite side.
187             handleSwipeFromClosedState(ev, animView, velocity, menuRow);
188         }
189     }
190 
handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)191     private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity,
192             NotificationMenuRowPlugin menuRow) {
193         boolean isDismissGesture = isDismissGesture(ev);
194         final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity);
195         final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity);
196 
197         final double timeForGesture = ev.getEventTime() - ev.getDownTime();
198         final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed()
199                 && timeForGesture >= SWIPE_MENU_TIMING;
200 
201         boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture;
202         boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing;
203         boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe;
204         boolean isFastNonDismissGesture =
205                 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture;
206         boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen()
207                 || mIsExpanded && !mPulsing;
208         boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough
209                 || (isFastNonDismissGesture && isAbleToShowMenu);
210         int menuSnapTarget = menuRow.getMenuSnapTarget();
211         boolean isNonFalseMenuRevealingGesture =
212                 !isFalseGesture(ev) && isMenuRevealingGestureAwayFromMenu;
213         if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture)
214                 && menuSnapTarget != 0) {
215             // Menu has not been snapped to previously and this is menu revealing gesture
216             snapOpen(animView, menuSnapTarget, velocity);
217             menuRow.onSnapOpen();
218         } else if (isDismissGesture(ev) && !gestureTowardsMenu) {
219             dismiss(animView, velocity);
220             menuRow.onDismiss();
221         } else {
222             snapClosed(animView, velocity);
223             menuRow.onSnapClosed();
224         }
225     }
226 
handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)227     private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity,
228             NotificationMenuRowPlugin menuRow) {
229         boolean isDismissGesture = isDismissGesture(ev);
230 
231         final boolean withinSnapMenuThreshold =
232                 menuRow.isWithinSnapMenuThreshold();
233 
234         if (withinSnapMenuThreshold && !isDismissGesture) {
235             // Haven't moved enough to unsnap from the menu
236             menuRow.onSnapOpen();
237             snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
238         } else if (isDismissGesture && !menuRow.shouldSnapBack()) {
239             // Only dismiss if we're not moving towards the menu
240             dismiss(animView, velocity);
241             menuRow.onDismiss();
242         } else {
243             snapClosed(animView, velocity);
244             menuRow.onSnapClosed();
245         }
246     }
247 
248     @Override
dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)249     public void dismissChild(final View view, float velocity,
250             boolean useAccelerateInterpolator) {
251         superDismissChild(view, velocity, useAccelerateInterpolator);
252         if (mCallback.shouldDismissQuickly()) {
253             // We don't want to quick-dismiss when it's a heads up as this might lead to closing
254             // of the panel early.
255             mCallback.handleChildViewDismissed(view);
256         }
257         mCallback.onDismiss();
258         handleMenuCoveredOrDismissed();
259     }
260 
261     @VisibleForTesting
superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator)262     protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
263         super.dismissChild(view, velocity, useAccelerateInterpolator);
264     }
265 
266     @VisibleForTesting
superSnapChild(final View animView, final float targetLeft, float velocity)267     protected void superSnapChild(final View animView, final float targetLeft, float velocity) {
268         super.snapChild(animView, targetLeft, velocity);
269     }
270 
271     @Override
snapChild(final View animView, final float targetLeft, float velocity)272     public void snapChild(final View animView, final float targetLeft, float velocity) {
273         superSnapChild(animView, targetLeft, velocity);
274         mCallback.onDragCancelled(animView);
275         if (targetLeft == 0) {
276             handleMenuCoveredOrDismissed();
277         }
278     }
279 
280     @Override
snooze(StatusBarNotification sbn, SnoozeOption snoozeOption)281     public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
282         mCallback.onSnooze(sbn, snoozeOption);
283     }
284 
285     @Override
snooze(StatusBarNotification sbn, int hours)286     public void snooze(StatusBarNotification sbn, int hours) {
287         mCallback.onSnooze(sbn, hours);
288     }
289 
290     @VisibleForTesting
handleMenuCoveredOrDismissed()291     protected void handleMenuCoveredOrDismissed() {
292         View exposedMenuView = getExposedMenuView();
293         if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) {
294             clearExposedMenuView();
295         }
296     }
297 
298     @VisibleForTesting
superGetViewTranslationAnimator(View v, float target, ValueAnimator.AnimatorUpdateListener listener)299     protected Animator superGetViewTranslationAnimator(View v, float target,
300             ValueAnimator.AnimatorUpdateListener listener) {
301         return super.getViewTranslationAnimator(v, target, listener);
302     }
303 
304     @Override
getViewTranslationAnimator(View v, float target, ValueAnimator.AnimatorUpdateListener listener)305     public Animator getViewTranslationAnimator(View v, float target,
306             ValueAnimator.AnimatorUpdateListener listener) {
307         if (v instanceof ExpandableNotificationRow) {
308             return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener);
309         } else {
310             return superGetViewTranslationAnimator(v, target, listener);
311         }
312     }
313 
314     @Override
setTranslation(View v, float translate)315     public void setTranslation(View v, float translate) {
316         if (v instanceof SwipeableView) {
317             ((SwipeableView) v).setTranslation(translate);
318         }
319     }
320 
321     @Override
getTranslation(View v)322     public float getTranslation(View v) {
323         if (v instanceof SwipeableView) {
324             return ((SwipeableView) v).getTranslation();
325         }
326         else {
327             return 0f;
328         }
329     }
330 
331     @Override
swipedFastEnough(float translation, float viewSize)332     public boolean swipedFastEnough(float translation, float viewSize) {
333         return swipedFastEnough();
334     }
335 
336     @Override
337     @VisibleForTesting
swipedFastEnough()338     protected boolean swipedFastEnough() {
339         return super.swipedFastEnough();
340     }
341 
342     @Override
swipedFarEnough(float translation, float viewSize)343     public boolean swipedFarEnough(float translation, float viewSize) {
344         return swipedFarEnough();
345     }
346 
347     @Override
348     @VisibleForTesting
swipedFarEnough()349     protected boolean swipedFarEnough() {
350         return super.swipedFarEnough();
351     }
352 
353     @Override
dismiss(View animView, float velocity)354     public void dismiss(View animView, float velocity) {
355         dismissChild(animView, velocity,
356                 !swipedFastEnough() /* useAccelerateInterpolator */);
357     }
358 
359     @Override
snapOpen(View animView, int targetLeft, float velocity)360     public void snapOpen(View animView, int targetLeft, float velocity) {
361         snapChild(animView, targetLeft, velocity);
362     }
363 
364     @VisibleForTesting
snapClosed(View animView, float velocity)365     protected void snapClosed(View animView, float velocity) {
366         snapChild(animView, 0, velocity);
367     }
368 
369     @Override
370     @VisibleForTesting
getEscapeVelocity()371     protected float getEscapeVelocity() {
372         return super.getEscapeVelocity();
373     }
374 
375     @Override
getMinDismissVelocity()376     public float getMinDismissVelocity() {
377         return getEscapeVelocity();
378     }
379 
onMenuShown(View animView)380     public void onMenuShown(View animView) {
381         setExposedMenuView(getTranslatingParentView());
382         mCallback.onDragCancelled(animView);
383         Handler handler = getHandler();
384 
385         // If we're on the lockscreen we want to false this.
386         if (mCallback.isAntiFalsingNeeded()) {
387             handler.removeCallbacks(getFalsingCheck());
388             handler.postDelayed(getFalsingCheck(), COVER_MENU_DELAY);
389         }
390     }
391 
392     @VisibleForTesting
shouldResetMenu(boolean force)393     protected boolean shouldResetMenu(boolean force) {
394         if (mMenuExposedView == null
395                 || (!force && mMenuExposedView == mTranslatingParentView)) {
396             // If no menu is showing or it's showing for this view we do nothing.
397             return false;
398         }
399         return true;
400     }
401 
resetExposedMenuView(boolean animate, boolean force)402     public void resetExposedMenuView(boolean animate, boolean force) {
403         if (!shouldResetMenu(force)) {
404             return;
405         }
406         final View prevMenuExposedView = getExposedMenuView();
407         if (animate) {
408             Animator anim = getViewTranslationAnimator(prevMenuExposedView,
409                     0 /* leftTarget */, null /* updateListener */);
410             if (anim != null) {
411                 anim.start();
412             }
413         } else if (prevMenuExposedView instanceof SwipeableView) {
414             SwipeableView row = (SwipeableView) prevMenuExposedView;
415             if (!row.isRemoved()) {
416                 row.resetTranslation();
417             }
418         }
419         clearExposedMenuView();
420     }
421 
isTouchInView(MotionEvent ev, View view)422     public static boolean isTouchInView(MotionEvent ev, View view) {
423         if (view == null) {
424             return false;
425         }
426         final int height = (view instanceof ExpandableView)
427                 ? ((ExpandableView) view).getActualHeight()
428                 : view.getHeight();
429         final int rx = (int) ev.getX();
430         final int ry = (int) ev.getY();
431         int[] temp = new int[2];
432         view.getLocationOnScreen(temp);
433         final int x = temp[0];
434         final int y = temp[1];
435         Rect rect = new Rect(x, y, x + view.getWidth(), y + height);
436         boolean ret = rect.contains(rx, ry);
437         return ret;
438     }
439 
setPulsing(boolean pulsing)440     public void setPulsing(boolean pulsing) {
441         mPulsing = pulsing;
442     }
443 
444     public interface NotificationCallback extends SwipeHelper.Callback{
445         /**
446          * @return if the view should be dismissed as soon as the touch is released, otherwise its
447          *         removed when the animation finishes.
448          */
shouldDismissQuickly()449         boolean shouldDismissQuickly();
450 
handleChildViewDismissed(View view)451         void handleChildViewDismissed(View view);
452 
onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption)453         void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption);
454 
onSnooze(StatusBarNotification sbn, int hours)455         void onSnooze(StatusBarNotification sbn, int hours);
456 
onDismiss()457         void onDismiss();
458     }
459 }
460