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