blob: 0968674d31cc651a33f66b43eb64c97c6eb1d572 [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;
Dave Mankoff781ef7e2019-06-28 16:33:25 -040031import com.android.systemui.plugins.FalsingManager;
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040032import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
33import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040034import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
35import com.android.systemui.statusbar.notification.row.ExpandableView;
36
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040037class 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;
shawnlin3a950c32019-05-15 20:06:10 +080051 private boolean mIsExpanded;
Selim Cinekd0b48e32019-05-24 20:49:23 -070052 private boolean mPulsing;
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040053
Dave Mankoff781ef7e2019-06-28 16:33:25 -040054 NotificationSwipeHelper(
55 int swipeDirection, NotificationCallback callback, Context context,
56 NotificationMenuRowPlugin.OnMenuEventListener menuListener,
57 FalsingManager falsingManager) {
58 super(swipeDirection, callback, context, falsingManager);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -040059 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
69 public View getTranslatingParentView() {
70 return mTranslatingParentView;
71 }
72
73 public void clearTranslatingParentView() { setTranslatingParentView(null); }
74
75 @VisibleForTesting
76 protected void setTranslatingParentView(View view) { mTranslatingParentView = view; };
77
78 public void setExposedMenuView(View view) {
79 mMenuExposedView = view;
80 }
81
82 public void clearExposedMenuView() { setExposedMenuView(null); }
83
84 public void clearCurrentMenuRow() { setCurrentMenuRow(null); }
85
86 public View getExposedMenuView() {
87 return mMenuExposedView;
88 }
89
90 public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) {
91 mCurrMenuRow = menuRow;
92 }
93
94 public NotificationMenuRowPlugin getCurrentMenuRow() { return mCurrMenuRow; }
95
96 @VisibleForTesting
97 protected Handler getHandler() { return mHandler; }
98
99 @VisibleForTesting
Evan Lairde55c6012019-03-13 12:54:37 -0400100 protected Runnable getFalsingCheck() {
101 return mFalsingCheck;
102 }
103
shawnlin3a950c32019-05-15 20:06:10 +0800104 public void setIsExpanded(boolean isExpanded) {
105 mIsExpanded = isExpanded;
106 }
107
Evan Lairde55c6012019-03-13 12:54:37 -0400108 @Override
109 protected void onChildSnappedBack(View animView, float targetLeft) {
110 if (mCurrMenuRow != null && targetLeft == 0) {
111 mCurrMenuRow.resetMenu();
112 clearCurrentMenuRow();
113 }
114 }
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400115
116 @Override
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
135 protected void initializeRow(ExpandableNotificationRow row) {
136 if (row.getEntry().hasFinishedInitialization()) {
137 mCurrMenuRow = row.createMenu();
Evan Lairde55c6012019-03-13 12:54:37 -0400138 if (mCurrMenuRow != null) {
139 mCurrMenuRow.setMenuClickListener(mMenuListener);
140 mCurrMenuRow.onTouchStart();
141 }
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400142 }
143 }
144
145 private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
146 return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
147 }
148
149 @Override
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
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
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
Gus Prevas2e02a832018-11-15 13:48:56 -0500188 handleSwipeFromOpenState(ev, animView, velocity, menuRow);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400189 } else {
190 // Menu has not been snapped, or was snapped previously but is now on
191 // the opposite side.
Gus Prevas2e02a832018-11-15 13:48:56 -0500192 handleSwipeFromClosedState(ev, animView, velocity, menuRow);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400193 }
194 }
195
Gus Prevas2e02a832018-11-15 13:48:56 -0500196 private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity,
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400197 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
Gus Prevas2e02a832018-11-15 13:48:56 -0500206 boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture;
207 boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing;
208 boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe;
209 boolean isFastNonDismissGesture =
210 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture;
Selim Cinekd0b48e32019-05-24 20:49:23 -0700211 boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen()
212 || mIsExpanded && !mPulsing;
shawnlin3a950c32019-05-15 20:06:10 +0800213 boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough
214 || (isFastNonDismissGesture && isAbleToShowMenu);
Gus Prevasfe15aa1f2019-01-04 15:13:21 -0500215 int menuSnapTarget = menuRow.getMenuSnapTarget();
216 boolean isNonFalseMenuRevealingGesture =
217 !isFalseGesture(ev) && isMenuRevealingGestureAwayFromMenu;
218 if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture)
219 && menuSnapTarget != 0) {
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400220 // Menu has not been snapped to previously and this is menu revealing gesture
Gus Prevasfe15aa1f2019-01-04 15:13:21 -0500221 snapOpen(animView, menuSnapTarget, velocity);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400222 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
Gus Prevas2e02a832018-11-15 13:48:56 -0500232 private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity,
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400233 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
254 public void dismissChild(final View view, float velocity,
255 boolean useAccelerateInterpolator) {
256 superDismissChild(view, velocity, useAccelerateInterpolator);
Selim Cinekae55d832019-02-22 17:43:43 -0800257 if (mCallback.shouldDismissQuickly()) {
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400258 // 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
267 protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
268 super.dismissChild(view, velocity, useAccelerateInterpolator);
269 }
270
271 @VisibleForTesting
272 protected void superSnapChild(final View animView, final float targetLeft, float velocity) {
273 super.snapChild(animView, targetLeft, velocity);
274 }
275
276 @Override
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
286 public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
287 mCallback.onSnooze(sbn, snoozeOption);
288 }
289
290 @VisibleForTesting
291 protected void handleMenuCoveredOrDismissed() {
292 View exposedMenuView = getExposedMenuView();
293 if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) {
294 clearExposedMenuView();
295 }
296 }
297
298 @VisibleForTesting
299 protected Animator superGetViewTranslationAnimator(View v, float target,
300 ValueAnimator.AnimatorUpdateListener listener) {
301 return super.getViewTranslationAnimator(v, target, listener);
302 }
303
304 @Override
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
315 public void setTranslation(View v, float translate) {
316 if (v instanceof ExpandableNotificationRow) {
317 ((ExpandableNotificationRow) v).setTranslation(translate);
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400318 }
319 }
320
321 @Override
322 public float getTranslation(View v) {
323 if (v instanceof ExpandableNotificationRow) {
324 return ((ExpandableNotificationRow) v).getTranslation();
325 }
326 else {
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400327 return 0f;
328 }
329 }
330
331 @Override
332 public boolean swipedFastEnough(float translation, float viewSize) {
333 return swipedFastEnough();
334 }
335
336 @Override
337 @VisibleForTesting
338 protected boolean swipedFastEnough() {
339 return super.swipedFastEnough();
340 }
341
342 @Override
343 public boolean swipedFarEnough(float translation, float viewSize) {
344 return swipedFarEnough();
345 }
346
347 @Override
348 @VisibleForTesting
349 protected boolean swipedFarEnough() {
350 return super.swipedFarEnough();
351 }
352
353 @Override
354 public void dismiss(View animView, float velocity) {
355 dismissChild(animView, velocity,
356 !swipedFastEnough() /* useAccelerateInterpolator */);
357 }
358
359 @Override
360 public void snapOpen(View animView, int targetLeft, float velocity) {
361 snapChild(animView, targetLeft, velocity);
362 }
363
364 @VisibleForTesting
365 protected void snapClosed(View animView, float velocity) {
366 snapChild(animView, 0, velocity);
367 }
368
369 @Override
370 @VisibleForTesting
371 protected float getEscapeVelocity() {
372 return super.getEscapeVelocity();
373 }
374
375 @Override
376 public float getMinDismissVelocity() {
377 return getEscapeVelocity();
378 }
379
380 public void onMenuShown(View animView) {
381 setExposedMenuView(getTranslatingParentView());
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400382 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
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
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
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
Selim Cinekd0b48e32019-05-24 20:49:23 -0700440 public void setPulsing(boolean pulsing) {
441 mPulsing = pulsing;
442 }
443
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400444 public interface NotificationCallback extends SwipeHelper.Callback{
Selim Cinekae55d832019-02-22 17:43:43 -0800445 /**
446 * @return if the view should be dismissed as soon as the touch is released, otherwise its
447 * removed when the animation finishes.
448 */
449 boolean shouldDismissQuickly();
Aaron Heuckroth45d20be2018-09-18 13:47:26 -0400450
451 void handleChildViewDismissed(View view);
452
453 void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption);
454
455 void onDismiss();
456 }
Gus Prevas2e02a832018-11-15 13:48:56 -0500457}