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
38         implements NotificationSwipeActionHelper {
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 = new Runnable() {
62             @Override
63             public void run() {
64                 resetExposedMenuView(true /* animate */, true /* force */);
65             }
66         };
67     }
68 
getTranslatingParentView()69     public View getTranslatingParentView() {
70         return mTranslatingParentView;
71     }
72 
clearTranslatingParentView()73     public void clearTranslatingParentView() { setTranslatingParentView(null); }
74 
75     @VisibleForTesting
setTranslatingParentView(View view)76     protected void setTranslatingParentView(View view) { mTranslatingParentView = view; };
77 
setExposedMenuView(View view)78     public void setExposedMenuView(View view) {
79         mMenuExposedView = view;
80     }
81 
clearExposedMenuView()82     public void clearExposedMenuView() { setExposedMenuView(null); }
83 
clearCurrentMenuRow()84     public void clearCurrentMenuRow() { setCurrentMenuRow(null); }
85 
getExposedMenuView()86     public View getExposedMenuView() {
87         return mMenuExposedView;
88     }
89 
setCurrentMenuRow(NotificationMenuRowPlugin menuRow)90     public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) {
91         mCurrMenuRow = menuRow;
92     }
93 
getCurrentMenuRow()94     public NotificationMenuRowPlugin getCurrentMenuRow() {  return mCurrMenuRow; }
95 
96     @VisibleForTesting
getHandler()97     protected Handler getHandler() { return mHandler; }
98 
99     @VisibleForTesting
getFalsingCheck()100     protected Runnable getFalsingCheck() {
101         return mFalsingCheck;
102     }
103 
setIsExpanded(boolean isExpanded)104     public void setIsExpanded(boolean isExpanded) {
105         mIsExpanded = isExpanded;
106     }
107 
108     @Override
onChildSnappedBack(View animView, float targetLeft)109     protected void onChildSnappedBack(View animView, float targetLeft) {
110         if (mCurrMenuRow != null && targetLeft == 0) {
111             mCurrMenuRow.resetMenu();
112             clearCurrentMenuRow();
113         }
114     }
115 
116     @Override
onDownUpdate(View currView, MotionEvent ev)117     public void onDownUpdate(View currView, MotionEvent ev) {
118         mTranslatingParentView = currView;
119         NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
120         if (menuRow != null) {
121             menuRow.onTouchStart();
122         }
123         clearCurrentMenuRow();
124         getHandler().removeCallbacks(getFalsingCheck());
125 
126         // Slide back any notifications that might be showing a menu
127         resetExposedMenuView(true /* animate */, false /* force */);
128 
129         if (currView instanceof ExpandableNotificationRow) {
130             initializeRow((ExpandableNotificationRow) currView);
131         }
132     }
133 
134     @VisibleForTesting
initializeRow(ExpandableNotificationRow row)135     protected void initializeRow(ExpandableNotificationRow row) {
136         if (row.getEntry().hasFinishedInitialization()) {
137             mCurrMenuRow = row.createMenu();
138             if (mCurrMenuRow != null) {
139                 mCurrMenuRow.setMenuClickListener(mMenuListener);
140                 mCurrMenuRow.onTouchStart();
141             }
142         }
143     }
144 
swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow)145     private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
146         return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
147     }
148 
149     @Override
onMoveUpdate(View view, MotionEvent ev, float translation, float delta)150     public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
151         getHandler().removeCallbacks(getFalsingCheck());
152         NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
153         if (menuRow != null) {
154             menuRow.onTouchMove(delta);
155         }
156     }
157 
158     @Override
handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)159     public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
160             float translation) {
161         NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
162         if (menuRow != null) {
163             menuRow.onTouchEnd();
164             handleMenuRowSwipe(ev, animView, velocity, menuRow);
165             return true;
166         }
167         return false;
168     }
169 
170     @VisibleForTesting
handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)171     protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity,
172             NotificationMenuRowPlugin menuRow) {
173         if (!menuRow.shouldShowMenu()) {
174             // If the menu should not be shown, then there is no need to check if the a swipe
175             // should result in a snapping to the menu. As a result, just check if the swipe
176             // was enough to dismiss the notification.
177             if (isDismissGesture(ev)) {
178                 dismiss(animView, velocity);
179             } else {
180                 snapClosed(animView, velocity);
181                 menuRow.onSnapClosed();
182             }
183             return;
184         }
185 
186         if (menuRow.isSnappedAndOnSameSide()) {
187             // Menu was snapped to previously and we're on the same side
188             handleSwipeFromOpenState(ev, animView, velocity, menuRow);
189         } else {
190             // Menu has not been snapped, or was snapped previously but is now on
191             // the opposite side.
192             handleSwipeFromClosedState(ev, animView, velocity, menuRow);
193         }
194     }
195 
handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)196     private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity,
197             NotificationMenuRowPlugin menuRow) {
198         boolean isDismissGesture = isDismissGesture(ev);
199         final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity);
200         final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity);
201 
202         final double timeForGesture = ev.getEventTime() - ev.getDownTime();
203         final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed()
204                 && timeForGesture >= SWIPE_MENU_TIMING;
205 
206         boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture;
207         boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing;
208         boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe;
209         boolean isFastNonDismissGesture =
210                 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture;
211         boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen()
212                 || mIsExpanded && !mPulsing;
213         boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough
214                 || (isFastNonDismissGesture && isAbleToShowMenu);
215         int menuSnapTarget = menuRow.getMenuSnapTarget();
216         boolean isNonFalseMenuRevealingGesture =
217                 !isFalseGesture(ev) && isMenuRevealingGestureAwayFromMenu;
218         if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture)
219                 && menuSnapTarget != 0) {
220             // Menu has not been snapped to previously and this is menu revealing gesture
221             snapOpen(animView, menuSnapTarget, velocity);
222             menuRow.onSnapOpen();
223         } else if (isDismissGesture(ev) && !gestureTowardsMenu) {
224             dismiss(animView, velocity);
225             menuRow.onDismiss();
226         } else {
227             snapClosed(animView, velocity);
228             menuRow.onSnapClosed();
229         }
230     }
231 
handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)232     private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity,
233             NotificationMenuRowPlugin menuRow) {
234         boolean isDismissGesture = isDismissGesture(ev);
235 
236         final boolean withinSnapMenuThreshold =
237                 menuRow.isWithinSnapMenuThreshold();
238 
239         if (withinSnapMenuThreshold && !isDismissGesture) {
240             // Haven't moved enough to unsnap from the menu
241             menuRow.onSnapOpen();
242             snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
243         } else if (isDismissGesture && !menuRow.shouldSnapBack()) {
244             // Only dismiss if we're not moving towards the menu
245             dismiss(animView, velocity);
246             menuRow.onDismiss();
247         } else {
248             snapClosed(animView, velocity);
249             menuRow.onSnapClosed();
250         }
251     }
252 
253     @Override
dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)254     public void dismissChild(final View view, float velocity,
255             boolean useAccelerateInterpolator) {
256         superDismissChild(view, velocity, useAccelerateInterpolator);
257         if (mCallback.shouldDismissQuickly()) {
258             // We don't want to quick-dismiss when it's a heads up as this might lead to closing
259             // of the panel early.
260             mCallback.handleChildViewDismissed(view);
261         }
262         mCallback.onDismiss();
263         handleMenuCoveredOrDismissed();
264     }
265 
266     @VisibleForTesting
superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator)267     protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
268         super.dismissChild(view, velocity, useAccelerateInterpolator);
269     }
270 
271     @VisibleForTesting
superSnapChild(final View animView, final float targetLeft, float velocity)272     protected void superSnapChild(final View animView, final float targetLeft, float velocity) {
273         super.snapChild(animView, targetLeft, velocity);
274     }
275 
276     @Override
snapChild(final View animView, final float targetLeft, float velocity)277     public void snapChild(final View animView, final float targetLeft, float velocity) {
278         superSnapChild(animView, targetLeft, velocity);
279         mCallback.onDragCancelled(animView);
280         if (targetLeft == 0) {
281             handleMenuCoveredOrDismissed();
282         }
283     }
284 
285     @Override
snooze(StatusBarNotification sbn, SnoozeOption snoozeOption)286     public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
287         mCallback.onSnooze(sbn, snoozeOption);
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 ExpandableNotificationRow) {
317             ((ExpandableNotificationRow) v).setTranslation(translate);
318         }
319     }
320 
321     @Override
getTranslation(View v)322     public float getTranslation(View v) {
323         if (v instanceof ExpandableNotificationRow) {
324             return ((ExpandableNotificationRow) 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 ExpandableNotificationRow) {
414             ExpandableNotificationRow row = (ExpandableNotificationRow) 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.getRawX();
430         final int ry = (int) ev.getRawY();
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 
onDismiss()455         void onDismiss();
456     }
457 }
458