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 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 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 = new Runnable() { 62 @Override 63 public void run() { 64 resetExposedMenuView(true /* animate */, true /* force */); 65 } 66 }; 67 } 68 getTranslatingParentView()69 public View getTranslatingParentView() { 70 return mTranslatingParentView; 71 } 72 clearTranslatingParentView()73 public void clearTranslatingParentView() { setTranslatingParentView(null); } 74 75 @VisibleForTesting setTranslatingParentView(View view)76 protected void setTranslatingParentView(View view) { mTranslatingParentView = view; }; 77 setExposedMenuView(View view)78 public void setExposedMenuView(View view) { 79 mMenuExposedView = view; 80 } 81 clearExposedMenuView()82 public void clearExposedMenuView() { setExposedMenuView(null); } 83 clearCurrentMenuRow()84 public void clearCurrentMenuRow() { setCurrentMenuRow(null); } 85 getExposedMenuView()86 public View getExposedMenuView() { 87 return mMenuExposedView; 88 } 89 setCurrentMenuRow(NotificationMenuRowPlugin menuRow)90 public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) { 91 mCurrMenuRow = menuRow; 92 } 93 getCurrentMenuRow()94 public NotificationMenuRowPlugin getCurrentMenuRow() { return mCurrMenuRow; } 95 96 @VisibleForTesting getHandler()97 protected Handler getHandler() { return mHandler; } 98 99 @VisibleForTesting getFalsingCheck()100 protected Runnable getFalsingCheck() { 101 return mFalsingCheck; 102 } 103 setIsExpanded(boolean isExpanded)104 public void setIsExpanded(boolean isExpanded) { 105 mIsExpanded = isExpanded; 106 } 107 108 @Override onChildSnappedBack(View animView, float targetLeft)109 protected void onChildSnappedBack(View animView, float targetLeft) { 110 if (mCurrMenuRow != null && targetLeft == 0) { 111 mCurrMenuRow.resetMenu(); 112 clearCurrentMenuRow(); 113 } 114 } 115 116 @Override onDownUpdate(View currView, MotionEvent ev)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 initializeRow(ExpandableNotificationRow row)135 protected void initializeRow(ExpandableNotificationRow row) { 136 if (row.getEntry().hasFinishedInitialization()) { 137 mCurrMenuRow = row.createMenu(); 138 if (mCurrMenuRow != null) { 139 mCurrMenuRow.setMenuClickListener(mMenuListener); 140 mCurrMenuRow.onTouchStart(); 141 } 142 } 143 } 144 swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow)145 private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) { 146 return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu(); 147 } 148 149 @Override onMoveUpdate(View view, MotionEvent ev, float translation, float delta)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 handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)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 handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)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 188 handleSwipeFromOpenState(ev, animView, velocity, menuRow); 189 } else { 190 // Menu has not been snapped, or was snapped previously but is now on 191 // the opposite side. 192 handleSwipeFromClosedState(ev, animView, velocity, menuRow); 193 } 194 } 195 handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)196 private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, 197 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 206 boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture; 207 boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing; 208 boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe; 209 boolean isFastNonDismissGesture = 210 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture; 211 boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen() 212 || mIsExpanded && !mPulsing; 213 boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough 214 || (isFastNonDismissGesture && isAbleToShowMenu); 215 int menuSnapTarget = menuRow.getMenuSnapTarget(); 216 boolean isNonFalseMenuRevealingGesture = 217 !isFalseGesture(ev) && isMenuRevealingGestureAwayFromMenu; 218 if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture) 219 && menuSnapTarget != 0) { 220 // Menu has not been snapped to previously and this is menu revealing gesture 221 snapOpen(animView, menuSnapTarget, velocity); 222 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 handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)232 private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, 233 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 dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)254 public void dismissChild(final View view, float velocity, 255 boolean useAccelerateInterpolator) { 256 superDismissChild(view, velocity, useAccelerateInterpolator); 257 if (mCallback.shouldDismissQuickly()) { 258 // 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 superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator)267 protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { 268 super.dismissChild(view, velocity, useAccelerateInterpolator); 269 } 270 271 @VisibleForTesting superSnapChild(final View animView, final float targetLeft, float velocity)272 protected void superSnapChild(final View animView, final float targetLeft, float velocity) { 273 super.snapChild(animView, targetLeft, velocity); 274 } 275 276 @Override snapChild(final View animView, final float targetLeft, float velocity)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 snooze(StatusBarNotification sbn, SnoozeOption snoozeOption)286 public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) { 287 mCallback.onSnooze(sbn, snoozeOption); 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 ExpandableNotificationRow) { 317 ((ExpandableNotificationRow) v).setTranslation(translate); 318 } 319 } 320 321 @Override getTranslation(View v)322 public float getTranslation(View v) { 323 if (v instanceof ExpandableNotificationRow) { 324 return ((ExpandableNotificationRow) 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 ExpandableNotificationRow) { 414 ExpandableNotificationRow row = (ExpandableNotificationRow) 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.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 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 onDismiss()455 void onDismiss(); 456 } 457 } 458