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