blob: 4dfc343df2836b094de08d403efd682e153addcf [file] [log] [blame]
Aaron Heuckroth45d20be2018-09-18 13:47:26 -04001/*
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
18package com.android.systemui.statusbar.notification.stack;
19
20import android.animation.Animator;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.graphics.Rect;
24import android.os.Handler;
25import android.service.notification.StatusBarNotification;
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040026import android.view.MotionEvent;
27import android.view.View;
28
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.systemui.SwipeHelper;
31import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
32import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040033import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
34import com.android.systemui.statusbar.notification.row.ExpandableView;
35
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040036class 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;
shawnlin3a950c32019-05-15 20:06:10 +080050 private boolean mIsExpanded;
Selim Cinekd0b48e32019-05-24 20:49:23 -070051 private boolean mPulsing;
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040052
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
66 public View getTranslatingParentView() {
67 return mTranslatingParentView;
68 }
69
70 public void clearTranslatingParentView() { setTranslatingParentView(null); }
71
72 @VisibleForTesting
73 protected void setTranslatingParentView(View view) { mTranslatingParentView = view; };
74
75 public void setExposedMenuView(View view) {
76 mMenuExposedView = view;
77 }
78
79 public void clearExposedMenuView() { setExposedMenuView(null); }
80
81 public void clearCurrentMenuRow() { setCurrentMenuRow(null); }
82
83 public View getExposedMenuView() {
84 return mMenuExposedView;
85 }
86
87 public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) {
88 mCurrMenuRow = menuRow;
89 }
90
91 public NotificationMenuRowPlugin getCurrentMenuRow() { return mCurrMenuRow; }
92
93 @VisibleForTesting
94 protected Handler getHandler() { return mHandler; }
95
96 @VisibleForTesting
Evan Lairde55c6012019-03-13 12:54:37 -040097 protected Runnable getFalsingCheck() {
98 return mFalsingCheck;
99 }
100
shawnlin3a950c32019-05-15 20:06:10 +0800101 public void setIsExpanded(boolean isExpanded) {
102 mIsExpanded = isExpanded;
103 }
104
Evan Lairde55c6012019-03-13 12:54:37 -0400105 @Override
106 protected void onChildSnappedBack(View animView, float targetLeft) {
107 if (mCurrMenuRow != null && targetLeft == 0) {
108 mCurrMenuRow.resetMenu();
109 clearCurrentMenuRow();
110 }
111 }
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400112
113 @Override
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
132 protected void initializeRow(ExpandableNotificationRow row) {
133 if (row.getEntry().hasFinishedInitialization()) {
134 mCurrMenuRow = row.createMenu();
Evan Lairde55c6012019-03-13 12:54:37 -0400135 if (mCurrMenuRow != null) {
136 mCurrMenuRow.setMenuClickListener(mMenuListener);
137 mCurrMenuRow.onTouchStart();
138 }
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400139 }
140 }
141
142 private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
143 return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
144 }
145
146 @Override
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
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
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
Gus Prevas2e02a832018-11-15 13:48:56 -0500185 handleSwipeFromOpenState(ev, animView, velocity, menuRow);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400186 } else {
187 // Menu has not been snapped, or was snapped previously but is now on
188 // the opposite side.
Gus Prevas2e02a832018-11-15 13:48:56 -0500189 handleSwipeFromClosedState(ev, animView, velocity, menuRow);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400190 }
191 }
192
Gus Prevas2e02a832018-11-15 13:48:56 -0500193 private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity,
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400194 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
Gus Prevas2e02a832018-11-15 13:48:56 -0500203 boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture;
204 boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing;
205 boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe;
206 boolean isFastNonDismissGesture =
207 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture;
Selim Cinekd0b48e32019-05-24 20:49:23 -0700208 boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen()
209 || mIsExpanded && !mPulsing;
shawnlin3a950c32019-05-15 20:06:10 +0800210 boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough
211 || (isFastNonDismissGesture && isAbleToShowMenu);
Gus Prevasfe15aa1f2019-01-04 15:13:21 -0500212 int menuSnapTarget = menuRow.getMenuSnapTarget();
213 boolean isNonFalseMenuRevealingGesture =
214 !isFalseGesture(ev) && isMenuRevealingGestureAwayFromMenu;
215 if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture)
216 && menuSnapTarget != 0) {
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400217 // Menu has not been snapped to previously and this is menu revealing gesture
Gus Prevasfe15aa1f2019-01-04 15:13:21 -0500218 snapOpen(animView, menuSnapTarget, velocity);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400219 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
Gus Prevas2e02a832018-11-15 13:48:56 -0500229 private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity,
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400230 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
251 public void dismissChild(final View view, float velocity,
252 boolean useAccelerateInterpolator) {
253 superDismissChild(view, velocity, useAccelerateInterpolator);
Selim Cinekae55d832019-02-22 17:43:43 -0800254 if (mCallback.shouldDismissQuickly()) {
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400255 // 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
264 protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
265 super.dismissChild(view, velocity, useAccelerateInterpolator);
266 }
267
268 @VisibleForTesting
269 protected void superSnapChild(final View animView, final float targetLeft, float velocity) {
270 super.snapChild(animView, targetLeft, velocity);
271 }
272
273 @Override
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
283 public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
284 mCallback.onSnooze(sbn, snoozeOption);
285 }
286
287 @VisibleForTesting
288 protected void handleMenuCoveredOrDismissed() {
289 View exposedMenuView = getExposedMenuView();
290 if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) {
291 clearExposedMenuView();
292 }
293 }
294
295 @VisibleForTesting
296 protected Animator superGetViewTranslationAnimator(View v, float target,
297 ValueAnimator.AnimatorUpdateListener listener) {
298 return super.getViewTranslationAnimator(v, target, listener);
299 }
300
301 @Override
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
312 public void setTranslation(View v, float translate) {
313 if (v instanceof ExpandableNotificationRow) {
314 ((ExpandableNotificationRow) v).setTranslation(translate);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400315 }
316 }
317
318 @Override
319 public float getTranslation(View v) {
320 if (v instanceof ExpandableNotificationRow) {
321 return ((ExpandableNotificationRow) v).getTranslation();
322 }
323 else {
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400324 return 0f;
325 }
326 }
327
328 @Override
329 public boolean swipedFastEnough(float translation, float viewSize) {
330 return swipedFastEnough();
331 }
332
333 @Override
334 @VisibleForTesting
335 protected boolean swipedFastEnough() {
336 return super.swipedFastEnough();
337 }
338
339 @Override
340 public boolean swipedFarEnough(float translation, float viewSize) {
341 return swipedFarEnough();
342 }
343
344 @Override
345 @VisibleForTesting
346 protected boolean swipedFarEnough() {
347 return super.swipedFarEnough();
348 }
349
350 @Override
351 public void dismiss(View animView, float velocity) {
352 dismissChild(animView, velocity,
353 !swipedFastEnough() /* useAccelerateInterpolator */);
354 }
355
356 @Override
357 public void snapOpen(View animView, int targetLeft, float velocity) {
358 snapChild(animView, targetLeft, velocity);
359 }
360
361 @VisibleForTesting
362 protected void snapClosed(View animView, float velocity) {
363 snapChild(animView, 0, velocity);
364 }
365
366 @Override
367 @VisibleForTesting
368 protected float getEscapeVelocity() {
369 return super.getEscapeVelocity();
370 }
371
372 @Override
373 public float getMinDismissVelocity() {
374 return getEscapeVelocity();
375 }
376
377 public void onMenuShown(View animView) {
378 setExposedMenuView(getTranslatingParentView());
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400379 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
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
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
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
Selim Cinekd0b48e32019-05-24 20:49:23 -0700437 public void setPulsing(boolean pulsing) {
438 mPulsing = pulsing;
439 }
440
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400441 public interface NotificationCallback extends SwipeHelper.Callback{
Selim Cinekae55d832019-02-22 17:43:43 -0800442 /**
443 * @return if the view should be dismissed as soon as the touch is released, otherwise its
444 * removed when the animation finishes.
445 */
446 boolean shouldDismissQuickly();
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400447
448 void handleChildViewDismissed(View view);
449
450 void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption);
451
452 void onDismiss();
453 }
Gus Prevas2e02a832018-11-15 13:48:56 -0500454}