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.FalsingManager; 32 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 33 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; 34 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 35 import com.android.systemui.statusbar.notification.row.ExpandableView; 36 37 class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeActionHelper { 38 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 private boolean mIsExpanded; 52 private boolean mPulsing; 53 NotificationSwipeHelper( int swipeDirection, NotificationCallback callback, Context context, NotificationMenuRowPlugin.OnMenuEventListener menuListener, FalsingManager falsingManager)54 NotificationSwipeHelper( 55 int swipeDirection, NotificationCallback callback, Context context, 56 NotificationMenuRowPlugin.OnMenuEventListener menuListener, 57 FalsingManager falsingManager) { 58 super(swipeDirection, callback, context, falsingManager); 59 mMenuListener = menuListener; 60 mCallback = callback; 61 mFalsingCheck = () -> resetExposedMenuView(true /* animate */, true /* force */); 62 } 63 getTranslatingParentView()64 public View getTranslatingParentView() { 65 return mTranslatingParentView; 66 } 67 clearTranslatingParentView()68 public void clearTranslatingParentView() { setTranslatingParentView(null); } 69 70 @VisibleForTesting setTranslatingParentView(View view)71 protected void setTranslatingParentView(View view) { mTranslatingParentView = view; }; 72 setExposedMenuView(View view)73 public void setExposedMenuView(View view) { 74 mMenuExposedView = view; 75 } 76 clearExposedMenuView()77 public void clearExposedMenuView() { setExposedMenuView(null); } 78 clearCurrentMenuRow()79 public void clearCurrentMenuRow() { setCurrentMenuRow(null); } 80 getExposedMenuView()81 public View getExposedMenuView() { 82 return mMenuExposedView; 83 } 84 setCurrentMenuRow(NotificationMenuRowPlugin menuRow)85 public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) { 86 mCurrMenuRow = menuRow; 87 } 88 getCurrentMenuRow()89 public NotificationMenuRowPlugin getCurrentMenuRow() { return mCurrMenuRow; } 90 91 @VisibleForTesting getHandler()92 protected Handler getHandler() { return mHandler; } 93 94 @VisibleForTesting getFalsingCheck()95 protected Runnable getFalsingCheck() { 96 return mFalsingCheck; 97 } 98 setIsExpanded(boolean isExpanded)99 public void setIsExpanded(boolean isExpanded) { 100 mIsExpanded = isExpanded; 101 } 102 103 @Override onChildSnappedBack(View animView, float targetLeft)104 protected void onChildSnappedBack(View animView, float targetLeft) { 105 if (mCurrMenuRow != null && targetLeft == 0) { 106 mCurrMenuRow.resetMenu(); 107 clearCurrentMenuRow(); 108 } 109 } 110 111 @Override onDownUpdate(View currView, MotionEvent ev)112 public void onDownUpdate(View currView, MotionEvent ev) { 113 mTranslatingParentView = currView; 114 NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 115 if (menuRow != null) { 116 menuRow.onTouchStart(); 117 } 118 clearCurrentMenuRow(); 119 getHandler().removeCallbacks(getFalsingCheck()); 120 121 // Slide back any notifications that might be showing a menu 122 resetExposedMenuView(true /* animate */, false /* force */); 123 124 if (currView instanceof SwipeableView) { 125 initializeRow((SwipeableView) currView); 126 } 127 } 128 129 @VisibleForTesting initializeRow(SwipeableView row)130 protected void initializeRow(SwipeableView row) { 131 if (row.hasFinishedInitialization()) { 132 mCurrMenuRow = row.createMenu(); 133 if (mCurrMenuRow != null) { 134 mCurrMenuRow.setMenuClickListener(mMenuListener); 135 mCurrMenuRow.onTouchStart(); 136 } 137 } 138 } 139 swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow)140 private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) { 141 return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu(); 142 } 143 144 @Override onMoveUpdate(View view, MotionEvent ev, float translation, float delta)145 public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) { 146 getHandler().removeCallbacks(getFalsingCheck()); 147 NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 148 if (menuRow != null) { 149 menuRow.onTouchMove(delta); 150 } 151 } 152 153 @Override handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)154 public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, 155 float translation) { 156 NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 157 if (menuRow != null) { 158 menuRow.onTouchEnd(); 159 handleMenuRowSwipe(ev, animView, velocity, menuRow); 160 return true; 161 } 162 return false; 163 } 164 165 @VisibleForTesting handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)166 protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, 167 NotificationMenuRowPlugin menuRow) { 168 if (!menuRow.shouldShowMenu()) { 169 // If the menu should not be shown, then there is no need to check if the a swipe 170 // should result in a snapping to the menu. As a result, just check if the swipe 171 // was enough to dismiss the notification. 172 if (isDismissGesture(ev)) { 173 dismiss(animView, velocity); 174 } else { 175 snapClosed(animView, velocity); 176 menuRow.onSnapClosed(); 177 } 178 return; 179 } 180 181 if (menuRow.isSnappedAndOnSameSide()) { 182 // Menu was snapped to previously and we're on the same side 183 handleSwipeFromOpenState(ev, animView, velocity, menuRow); 184 } else { 185 // Menu has not been snapped, or was snapped previously but is now on 186 // the opposite side. 187 handleSwipeFromClosedState(ev, animView, velocity, menuRow); 188 } 189 } 190 handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)191 private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, 192 NotificationMenuRowPlugin menuRow) { 193 boolean isDismissGesture = isDismissGesture(ev); 194 final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity); 195 final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity); 196 197 final double timeForGesture = ev.getEventTime() - ev.getDownTime(); 198 final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed() 199 && timeForGesture >= SWIPE_MENU_TIMING; 200 201 boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture; 202 boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing; 203 boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe; 204 boolean isFastNonDismissGesture = 205 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture; 206 boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen() 207 || mIsExpanded && !mPulsing; 208 boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough 209 || (isFastNonDismissGesture && isAbleToShowMenu); 210 int menuSnapTarget = menuRow.getMenuSnapTarget(); 211 boolean isNonFalseMenuRevealingGesture = 212 !isFalseGesture(ev) && isMenuRevealingGestureAwayFromMenu; 213 if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture) 214 && menuSnapTarget != 0) { 215 // Menu has not been snapped to previously and this is menu revealing gesture 216 snapOpen(animView, menuSnapTarget, velocity); 217 menuRow.onSnapOpen(); 218 } else if (isDismissGesture(ev) && !gestureTowardsMenu) { 219 dismiss(animView, velocity); 220 menuRow.onDismiss(); 221 } else { 222 snapClosed(animView, velocity); 223 menuRow.onSnapClosed(); 224 } 225 } 226 handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)227 private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, 228 NotificationMenuRowPlugin menuRow) { 229 boolean isDismissGesture = isDismissGesture(ev); 230 231 final boolean withinSnapMenuThreshold = 232 menuRow.isWithinSnapMenuThreshold(); 233 234 if (withinSnapMenuThreshold && !isDismissGesture) { 235 // Haven't moved enough to unsnap from the menu 236 menuRow.onSnapOpen(); 237 snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); 238 } else if (isDismissGesture && !menuRow.shouldSnapBack()) { 239 // Only dismiss if we're not moving towards the menu 240 dismiss(animView, velocity); 241 menuRow.onDismiss(); 242 } else { 243 snapClosed(animView, velocity); 244 menuRow.onSnapClosed(); 245 } 246 } 247 248 @Override dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)249 public void dismissChild(final View view, float velocity, 250 boolean useAccelerateInterpolator) { 251 superDismissChild(view, velocity, useAccelerateInterpolator); 252 if (mCallback.shouldDismissQuickly()) { 253 // We don't want to quick-dismiss when it's a heads up as this might lead to closing 254 // of the panel early. 255 mCallback.handleChildViewDismissed(view); 256 } 257 mCallback.onDismiss(); 258 handleMenuCoveredOrDismissed(); 259 } 260 261 @VisibleForTesting superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator)262 protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { 263 super.dismissChild(view, velocity, useAccelerateInterpolator); 264 } 265 266 @VisibleForTesting superSnapChild(final View animView, final float targetLeft, float velocity)267 protected void superSnapChild(final View animView, final float targetLeft, float velocity) { 268 super.snapChild(animView, targetLeft, velocity); 269 } 270 271 @Override snapChild(final View animView, final float targetLeft, float velocity)272 public void snapChild(final View animView, final float targetLeft, float velocity) { 273 superSnapChild(animView, targetLeft, velocity); 274 mCallback.onDragCancelled(animView); 275 if (targetLeft == 0) { 276 handleMenuCoveredOrDismissed(); 277 } 278 } 279 280 @Override snooze(StatusBarNotification sbn, SnoozeOption snoozeOption)281 public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) { 282 mCallback.onSnooze(sbn, snoozeOption); 283 } 284 285 @Override snooze(StatusBarNotification sbn, int hours)286 public void snooze(StatusBarNotification sbn, int hours) { 287 mCallback.onSnooze(sbn, hours); 288 } 289 290 @VisibleForTesting handleMenuCoveredOrDismissed()291 protected void handleMenuCoveredOrDismissed() { 292 View exposedMenuView = getExposedMenuView(); 293 if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) { 294 clearExposedMenuView(); 295 } 296 } 297 298 @VisibleForTesting superGetViewTranslationAnimator(View v, float target, ValueAnimator.AnimatorUpdateListener listener)299 protected Animator superGetViewTranslationAnimator(View v, float target, 300 ValueAnimator.AnimatorUpdateListener listener) { 301 return super.getViewTranslationAnimator(v, target, listener); 302 } 303 304 @Override getViewTranslationAnimator(View v, float target, ValueAnimator.AnimatorUpdateListener listener)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 setTranslation(View v, float translate)315 public void setTranslation(View v, float translate) { 316 if (v instanceof SwipeableView) { 317 ((SwipeableView) v).setTranslation(translate); 318 } 319 } 320 321 @Override getTranslation(View v)322 public float getTranslation(View v) { 323 if (v instanceof SwipeableView) { 324 return ((SwipeableView) v).getTranslation(); 325 } 326 else { 327 return 0f; 328 } 329 } 330 331 @Override swipedFastEnough(float translation, float viewSize)332 public boolean swipedFastEnough(float translation, float viewSize) { 333 return swipedFastEnough(); 334 } 335 336 @Override 337 @VisibleForTesting swipedFastEnough()338 protected boolean swipedFastEnough() { 339 return super.swipedFastEnough(); 340 } 341 342 @Override swipedFarEnough(float translation, float viewSize)343 public boolean swipedFarEnough(float translation, float viewSize) { 344 return swipedFarEnough(); 345 } 346 347 @Override 348 @VisibleForTesting swipedFarEnough()349 protected boolean swipedFarEnough() { 350 return super.swipedFarEnough(); 351 } 352 353 @Override dismiss(View animView, float velocity)354 public void dismiss(View animView, float velocity) { 355 dismissChild(animView, velocity, 356 !swipedFastEnough() /* useAccelerateInterpolator */); 357 } 358 359 @Override snapOpen(View animView, int targetLeft, float velocity)360 public void snapOpen(View animView, int targetLeft, float velocity) { 361 snapChild(animView, targetLeft, velocity); 362 } 363 364 @VisibleForTesting snapClosed(View animView, float velocity)365 protected void snapClosed(View animView, float velocity) { 366 snapChild(animView, 0, velocity); 367 } 368 369 @Override 370 @VisibleForTesting getEscapeVelocity()371 protected float getEscapeVelocity() { 372 return super.getEscapeVelocity(); 373 } 374 375 @Override getMinDismissVelocity()376 public float getMinDismissVelocity() { 377 return getEscapeVelocity(); 378 } 379 onMenuShown(View animView)380 public void onMenuShown(View animView) { 381 setExposedMenuView(getTranslatingParentView()); 382 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 shouldResetMenu(boolean force)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 resetExposedMenuView(boolean animate, boolean force)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 SwipeableView) { 414 SwipeableView row = (SwipeableView) prevMenuExposedView; 415 if (!row.isRemoved()) { 416 row.resetTranslation(); 417 } 418 } 419 clearExposedMenuView(); 420 } 421 isTouchInView(MotionEvent ev, View view)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.getX(); 430 final int ry = (int) ev.getY(); 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 setPulsing(boolean pulsing)440 public void setPulsing(boolean pulsing) { 441 mPulsing = pulsing; 442 } 443 444 public interface NotificationCallback extends SwipeHelper.Callback{ 445 /** 446 * @return if the view should be dismissed as soon as the touch is released, otherwise its 447 * removed when the animation finishes. 448 */ shouldDismissQuickly()449 boolean shouldDismissQuickly(); 450 handleChildViewDismissed(View view)451 void handleChildViewDismissed(View view); 452 onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption)453 void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption); 454 onSnooze(StatusBarNotification sbn, int hours)455 void onSnooze(StatusBarNotification sbn, int hours); 456 onDismiss()457 void onDismiss(); 458 } 459 } 460