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