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.res.Resources; 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 import android.view.ViewConfiguration; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.systemui.SwipeHelper; 32 import com.android.systemui.dagger.qualifiers.Main; 33 import com.android.systemui.plugins.FalsingManager; 34 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 35 import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; 36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 37 import com.android.systemui.statusbar.notification.row.ExpandableView; 38 39 import java.lang.ref.WeakReference; 40 41 import javax.inject.Inject; 42 43 class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeActionHelper { 44 45 @VisibleForTesting 46 protected static final long COVER_MENU_DELAY = 4000; 47 private static final String TAG = "NotificationSwipeHelper"; 48 private final Runnable mFalsingCheck; 49 private View mTranslatingParentView; 50 private View mMenuExposedView; 51 private final NotificationCallback mCallback; 52 private final NotificationMenuRowPlugin.OnMenuEventListener mMenuListener; 53 54 private static final long SWIPE_MENU_TIMING = 200; 55 56 // Hold a weak ref to the menu row so that it isn't accidentally retained in memory. The 57 // lifetime of the row should be the same as the ActivatableView, which is owned by the 58 // NotificationStackScrollLayout. If the notification isn't in the notification shade, then it 59 // isn't possible to swipe it and, so, this class doesn't need to "help." 60 private WeakReference<NotificationMenuRowPlugin> mCurrMenuRowRef; 61 private boolean mIsExpanded; 62 private boolean mPulsing; 63 NotificationSwipeHelper( Resources resources, ViewConfiguration viewConfiguration, FalsingManager falsingManager, int swipeDirection, NotificationCallback callback, NotificationMenuRowPlugin.OnMenuEventListener menuListener)64 NotificationSwipeHelper( 65 Resources resources, ViewConfiguration viewConfiguration, 66 FalsingManager falsingManager, int swipeDirection, NotificationCallback callback, 67 NotificationMenuRowPlugin.OnMenuEventListener menuListener) { 68 super(swipeDirection, callback, resources, viewConfiguration, falsingManager); 69 mMenuListener = menuListener; 70 mCallback = callback; 71 mFalsingCheck = () -> resetExposedMenuView(true /* animate */, true /* force */); 72 } 73 getTranslatingParentView()74 public View getTranslatingParentView() { 75 return mTranslatingParentView; 76 } 77 clearTranslatingParentView()78 public void clearTranslatingParentView() { setTranslatingParentView(null); } 79 80 @VisibleForTesting setTranslatingParentView(View view)81 protected void setTranslatingParentView(View view) { mTranslatingParentView = view; } 82 setExposedMenuView(View view)83 public void setExposedMenuView(View view) { 84 mMenuExposedView = view; 85 } 86 clearExposedMenuView()87 public void clearExposedMenuView() { setExposedMenuView(null); } 88 clearCurrentMenuRow()89 public void clearCurrentMenuRow() { setCurrentMenuRow(null); } 90 getExposedMenuView()91 public View getExposedMenuView() { 92 return mMenuExposedView; 93 } 94 95 @VisibleForTesting setCurrentMenuRow(NotificationMenuRowPlugin menuRow)96 void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) { 97 mCurrMenuRowRef = menuRow != null ? new WeakReference<>(menuRow) : null; 98 } 99 getCurrentMenuRow()100 public NotificationMenuRowPlugin getCurrentMenuRow() { 101 if (mCurrMenuRowRef == null) { 102 return null; 103 } 104 return mCurrMenuRowRef.get(); 105 } 106 107 @VisibleForTesting getHandler()108 protected Handler getHandler() { return mHandler; } 109 110 @VisibleForTesting getFalsingCheck()111 protected Runnable getFalsingCheck() { 112 return mFalsingCheck; 113 } 114 setIsExpanded(boolean isExpanded)115 public void setIsExpanded(boolean isExpanded) { 116 mIsExpanded = isExpanded; 117 } 118 119 @Override onChildSnappedBack(View animView, float targetLeft)120 protected void onChildSnappedBack(View animView, float targetLeft) { 121 final NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 122 if (menuRow != null && targetLeft == 0) { 123 menuRow.resetMenu(); 124 clearCurrentMenuRow(); 125 } 126 } 127 128 @Override onDownUpdate(View currView, MotionEvent ev)129 public void onDownUpdate(View currView, MotionEvent ev) { 130 mTranslatingParentView = currView; 131 NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 132 if (menuRow != null) { 133 menuRow.onTouchStart(); 134 } 135 clearCurrentMenuRow(); 136 getHandler().removeCallbacks(getFalsingCheck()); 137 138 // Slide back any notifications that might be showing a menu 139 resetExposedMenuView(true /* animate */, false /* force */); 140 141 if (currView instanceof SwipeableView) { 142 initializeRow((SwipeableView) currView); 143 } 144 } 145 146 @VisibleForTesting initializeRow(SwipeableView row)147 protected void initializeRow(SwipeableView row) { 148 if (row.hasFinishedInitialization()) { 149 final NotificationMenuRowPlugin menuRow = row.createMenu(); 150 setCurrentMenuRow(menuRow); 151 if (menuRow != null) { 152 menuRow.setMenuClickListener(mMenuListener); 153 menuRow.onTouchStart(); 154 } 155 } 156 } 157 swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow)158 private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) { 159 return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu(); 160 } 161 162 @Override onMoveUpdate(View view, MotionEvent ev, float translation, float delta)163 public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) { 164 getHandler().removeCallbacks(getFalsingCheck()); 165 NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 166 if (menuRow != null) { 167 menuRow.onTouchMove(delta); 168 } 169 } 170 171 @Override handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)172 public boolean handleUpEvent(MotionEvent ev, View animView, float velocity, 173 float translation) { 174 NotificationMenuRowPlugin menuRow = getCurrentMenuRow(); 175 if (menuRow != null) { 176 menuRow.onTouchEnd(); 177 handleMenuRowSwipe(ev, animView, velocity, menuRow); 178 return true; 179 } 180 return false; 181 } 182 183 @VisibleForTesting handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)184 protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity, 185 NotificationMenuRowPlugin menuRow) { 186 if (!menuRow.shouldShowMenu()) { 187 // If the menu should not be shown, then there is no need to check if the a swipe 188 // should result in a snapping to the menu. As a result, just check if the swipe 189 // was enough to dismiss the notification. 190 if (isDismissGesture(ev)) { 191 dismiss(animView, velocity); 192 } else { 193 snapClosed(animView, velocity); 194 menuRow.onSnapClosed(); 195 } 196 return; 197 } 198 199 if (menuRow.isSnappedAndOnSameSide()) { 200 // Menu was snapped to previously and we're on the same side 201 handleSwipeFromOpenState(ev, animView, velocity, menuRow); 202 } else { 203 // Menu has not been snapped, or was snapped previously but is now on 204 // the opposite side. 205 handleSwipeFromClosedState(ev, animView, velocity, menuRow); 206 } 207 } 208 handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)209 private void handleSwipeFromClosedState(MotionEvent ev, View animView, float velocity, 210 NotificationMenuRowPlugin menuRow) { 211 boolean isDismissGesture = isDismissGesture(ev); 212 final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity); 213 final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity); 214 215 final double timeForGesture = ev.getEventTime() - ev.getDownTime(); 216 final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed() 217 && timeForGesture >= SWIPE_MENU_TIMING; 218 219 boolean isNonDismissGestureTowardsMenu = gestureTowardsMenu && !isDismissGesture; 220 boolean isSlowSwipe = !gestureFastEnough || showMenuForSlowOnGoing; 221 boolean slowSwipedFarEnough = swipedEnoughToShowMenu(menuRow) && isSlowSwipe; 222 boolean isFastNonDismissGesture = 223 gestureFastEnough && !gestureTowardsMenu && !isDismissGesture; 224 boolean isAbleToShowMenu = menuRow.shouldShowGutsOnSnapOpen() 225 || mIsExpanded && !mPulsing; 226 boolean isMenuRevealingGestureAwayFromMenu = slowSwipedFarEnough 227 || (isFastNonDismissGesture && isAbleToShowMenu); 228 int menuSnapTarget = menuRow.getMenuSnapTarget(); 229 boolean isNonFalseMenuRevealingGesture = 230 !isFalseGesture() && isMenuRevealingGestureAwayFromMenu; 231 if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture) 232 && menuSnapTarget != 0) { 233 // Menu has not been snapped to previously and this is menu revealing gesture 234 snapOpen(animView, menuSnapTarget, velocity); 235 menuRow.onSnapOpen(); 236 } else if (isDismissGesture(ev) && !gestureTowardsMenu) { 237 dismiss(animView, velocity); 238 menuRow.onDismiss(); 239 } else { 240 snapClosed(animView, velocity); 241 menuRow.onSnapClosed(); 242 } 243 } 244 handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow)245 private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, 246 NotificationMenuRowPlugin menuRow) { 247 boolean isDismissGesture = isDismissGesture(ev); 248 249 final boolean withinSnapMenuThreshold = 250 menuRow.isWithinSnapMenuThreshold(); 251 252 if (withinSnapMenuThreshold && !isDismissGesture) { 253 // Haven't moved enough to unsnap from the menu 254 menuRow.onSnapOpen(); 255 snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); 256 } else if (isDismissGesture && !menuRow.shouldSnapBack()) { 257 // Only dismiss if we're not moving towards the menu 258 dismiss(animView, velocity); 259 menuRow.onDismiss(); 260 } else { 261 snapClosed(animView, velocity); 262 menuRow.onSnapClosed(); 263 } 264 } 265 266 @Override dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)267 public void dismissChild(final View view, float velocity, 268 boolean useAccelerateInterpolator) { 269 superDismissChild(view, velocity, useAccelerateInterpolator); 270 if (mCallback.shouldDismissQuickly()) { 271 // We don't want to quick-dismiss when it's a heads up as this might lead to closing 272 // of the panel early. 273 mCallback.handleChildViewDismissed(view); 274 } 275 mCallback.onDismiss(); 276 handleMenuCoveredOrDismissed(); 277 } 278 279 @VisibleForTesting superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator)280 protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { 281 super.dismissChild(view, velocity, useAccelerateInterpolator); 282 } 283 284 @VisibleForTesting superSnapChild(final View animView, final float targetLeft, float velocity)285 protected void superSnapChild(final View animView, final float targetLeft, float velocity) { 286 super.snapChild(animView, targetLeft, velocity); 287 } 288 289 @Override snapChild(final View animView, final float targetLeft, float velocity)290 public void snapChild(final View animView, final float targetLeft, float velocity) { 291 superSnapChild(animView, targetLeft, velocity); 292 mCallback.onDragCancelled(animView); 293 if (targetLeft == 0) { 294 handleMenuCoveredOrDismissed(); 295 } 296 } 297 298 @Override snooze(StatusBarNotification sbn, SnoozeOption snoozeOption)299 public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) { 300 mCallback.onSnooze(sbn, snoozeOption); 301 } 302 303 @VisibleForTesting handleMenuCoveredOrDismissed()304 protected void handleMenuCoveredOrDismissed() { 305 View exposedMenuView = getExposedMenuView(); 306 if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) { 307 clearExposedMenuView(); 308 } 309 } 310 311 @VisibleForTesting superGetViewTranslationAnimator(View v, float target, ValueAnimator.AnimatorUpdateListener listener)312 protected Animator superGetViewTranslationAnimator(View v, float target, 313 ValueAnimator.AnimatorUpdateListener listener) { 314 return super.getViewTranslationAnimator(v, target, listener); 315 } 316 317 @Override getViewTranslationAnimator(View v, float target, ValueAnimator.AnimatorUpdateListener listener)318 public Animator getViewTranslationAnimator(View v, float target, 319 ValueAnimator.AnimatorUpdateListener listener) { 320 if (v instanceof ExpandableNotificationRow) { 321 return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener); 322 } else { 323 return superGetViewTranslationAnimator(v, target, listener); 324 } 325 } 326 327 @Override getTotalTranslationLength(View animView)328 protected float getTotalTranslationLength(View animView) { 329 return mCallback.getTotalTranslationLength(animView); 330 } 331 332 @Override setTranslation(View v, float translate)333 public void setTranslation(View v, float translate) { 334 if (v instanceof SwipeableView) { 335 ((SwipeableView) v).setTranslation(translate); 336 } 337 } 338 339 @Override getTranslation(View v)340 public float getTranslation(View v) { 341 if (v instanceof SwipeableView) { 342 return ((SwipeableView) v).getTranslation(); 343 } 344 else { 345 return 0f; 346 } 347 } 348 349 @Override swipedFastEnough(float translation, float viewSize)350 public boolean swipedFastEnough(float translation, float viewSize) { 351 return swipedFastEnough(); 352 } 353 354 @Override 355 @VisibleForTesting swipedFastEnough()356 protected boolean swipedFastEnough() { 357 return super.swipedFastEnough(); 358 } 359 360 @Override swipedFarEnough(float translation, float viewSize)361 public boolean swipedFarEnough(float translation, float viewSize) { 362 return swipedFarEnough(); 363 } 364 365 @Override 366 @VisibleForTesting swipedFarEnough()367 protected boolean swipedFarEnough() { 368 return super.swipedFarEnough(); 369 } 370 371 @Override dismiss(View animView, float velocity)372 public void dismiss(View animView, float velocity) { 373 dismissChild(animView, velocity, 374 !swipedFastEnough() /* useAccelerateInterpolator */); 375 } 376 377 @Override snapOpen(View animView, int targetLeft, float velocity)378 public void snapOpen(View animView, int targetLeft, float velocity) { 379 snapChild(animView, targetLeft, velocity); 380 } 381 382 @VisibleForTesting snapClosed(View animView, float velocity)383 protected void snapClosed(View animView, float velocity) { 384 snapChild(animView, 0, velocity); 385 } 386 387 @Override 388 @VisibleForTesting getEscapeVelocity()389 protected float getEscapeVelocity() { 390 return super.getEscapeVelocity(); 391 } 392 393 @Override getMinDismissVelocity()394 public float getMinDismissVelocity() { 395 return getEscapeVelocity(); 396 } 397 onMenuShown(View animView)398 public void onMenuShown(View animView) { 399 setExposedMenuView(getTranslatingParentView()); 400 mCallback.onDragCancelled(animView); 401 Handler handler = getHandler(); 402 403 // If we're on the lockscreen we want to false this. 404 if (mCallback.isAntiFalsingNeeded()) { 405 handler.removeCallbacks(getFalsingCheck()); 406 handler.postDelayed(getFalsingCheck(), COVER_MENU_DELAY); 407 } 408 } 409 410 @VisibleForTesting shouldResetMenu(boolean force)411 protected boolean shouldResetMenu(boolean force) { 412 if (mMenuExposedView == null 413 || (!force && mMenuExposedView == mTranslatingParentView)) { 414 // If no menu is showing or it's showing for this view we do nothing. 415 return false; 416 } 417 return true; 418 } 419 resetExposedMenuView(boolean animate, boolean force)420 public void resetExposedMenuView(boolean animate, boolean force) { 421 if (!shouldResetMenu(force)) { 422 return; 423 } 424 final View prevMenuExposedView = getExposedMenuView(); 425 if (animate) { 426 Animator anim = getViewTranslationAnimator(prevMenuExposedView, 427 0 /* leftTarget */, null /* updateListener */); 428 if (anim != null) { 429 anim.start(); 430 } 431 } else if (prevMenuExposedView instanceof SwipeableView) { 432 SwipeableView row = (SwipeableView) prevMenuExposedView; 433 if (!row.isRemoved()) { 434 row.resetTranslation(); 435 } 436 } 437 clearExposedMenuView(); 438 } 439 isTouchInView(MotionEvent ev, View view)440 public static boolean isTouchInView(MotionEvent ev, View view) { 441 if (view == null) { 442 return false; 443 } 444 final int height = (view instanceof ExpandableView) 445 ? ((ExpandableView) view).getActualHeight() 446 : view.getHeight(); 447 final int rx = (int) ev.getX(); 448 final int ry = (int) ev.getY(); 449 int[] temp = new int[2]; 450 view.getLocationOnScreen(temp); 451 final int x = temp[0]; 452 final int y = temp[1]; 453 Rect rect = new Rect(x, y, x + view.getWidth(), y + height); 454 boolean ret = rect.contains(rx, ry); 455 return ret; 456 } 457 setPulsing(boolean pulsing)458 public void setPulsing(boolean pulsing) { 459 mPulsing = pulsing; 460 } 461 462 public interface NotificationCallback extends SwipeHelper.Callback{ 463 /** 464 * @return if the view should be dismissed as soon as the touch is released, otherwise its 465 * removed when the animation finishes. 466 */ shouldDismissQuickly()467 boolean shouldDismissQuickly(); 468 handleChildViewDismissed(View view)469 void handleChildViewDismissed(View view); 470 onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption)471 void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption); 472 onDismiss()473 void onDismiss(); 474 475 /** 476 * Get the total translation length where we want to swipe to when dismissing the view. By 477 * default this is the size of the view, but can also be larger. 478 * @param animView the view to ask about 479 */ getTotalTranslationLength(View animView)480 float getTotalTranslationLength(View animView); 481 } 482 483 static class Builder { 484 private final Resources mResources; 485 private final ViewConfiguration mViewConfiguration; 486 private final FalsingManager mFalsingManager; 487 private int mSwipeDirection; 488 private NotificationCallback mNotificationCallback; 489 private NotificationMenuRowPlugin.OnMenuEventListener mOnMenuEventListener; 490 491 @Inject Builder(@ain Resources resources, ViewConfiguration viewConfiguration, FalsingManager falsingManager)492 Builder(@Main Resources resources, ViewConfiguration viewConfiguration, 493 FalsingManager falsingManager) { 494 mResources = resources; 495 mViewConfiguration = viewConfiguration; 496 mFalsingManager = falsingManager; 497 } 498 setSwipeDirection(int swipeDirection)499 Builder setSwipeDirection(int swipeDirection) { 500 mSwipeDirection = swipeDirection; 501 return this; 502 } 503 setNotificationCallback(NotificationCallback notificationCallback)504 Builder setNotificationCallback(NotificationCallback notificationCallback) { 505 mNotificationCallback = notificationCallback; 506 return this; 507 } 508 setOnMenuEventListener( NotificationMenuRowPlugin.OnMenuEventListener onMenuEventListener)509 Builder setOnMenuEventListener( 510 NotificationMenuRowPlugin.OnMenuEventListener onMenuEventListener) { 511 mOnMenuEventListener = onMenuEventListener; 512 return this; 513 } 514 build()515 NotificationSwipeHelper build() { 516 return new NotificationSwipeHelper(mResources, mViewConfiguration, mFalsingManager, 517 mSwipeDirection, mNotificationCallback, mOnMenuEventListener); 518 } 519 } 520 } 521