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