1 /* 2 * Copyright (C) 2019 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 License 15 */ 16 17 package com.android.systemui.statusbar 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.content.res.Configuration 24 import android.os.PowerManager 25 import android.os.SystemClock 26 import android.util.IndentingPrintWriter 27 import android.view.MotionEvent 28 import android.view.VelocityTracker 29 import android.view.ViewConfiguration 30 import androidx.annotation.VisibleForTesting 31 import com.android.systemui.Dumpable 32 import com.android.systemui.Gefingerpoken 33 import com.android.systemui.R 34 import com.android.app.animation.Interpolators 35 import com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN 36 import com.android.systemui.classifier.FalsingCollector 37 import com.android.systemui.dagger.SysUISingleton 38 import com.android.systemui.dump.DumpManager 39 import com.android.systemui.plugins.FalsingManager 40 import com.android.systemui.plugins.statusbar.StatusBarStateController 41 import com.android.systemui.shade.ShadeExpansionStateManager 42 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator 43 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 44 import com.android.systemui.statusbar.notification.row.ExpandableView 45 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager 46 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 47 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone 48 import com.android.systemui.statusbar.phone.KeyguardBypassController 49 import com.android.systemui.statusbar.policy.ConfigurationController 50 import java.io.PrintWriter 51 import javax.inject.Inject 52 import kotlin.math.max 53 54 /** 55 * A utility class that handles notification panel expansion when a user swipes downward on a 56 * notification from the pulsing state. 57 * If face-bypass is enabled, the user can swipe down anywhere on the screen (not just from a 58 * notification) to trigger the notification panel expansion. 59 */ 60 @SysUISingleton 61 class PulseExpansionHandler @Inject 62 constructor( 63 context: Context, 64 private val wakeUpCoordinator: NotificationWakeUpCoordinator, 65 private val bypassController: KeyguardBypassController, 66 private val headsUpManager: HeadsUpManagerPhone, 67 private val roundnessManager: NotificationRoundnessManager, 68 configurationController: ConfigurationController, 69 private val statusBarStateController: StatusBarStateController, 70 private val falsingManager: FalsingManager, 71 shadeExpansionStateManager: ShadeExpansionStateManager, 72 private val lockscreenShadeTransitionController: LockscreenShadeTransitionController, 73 private val falsingCollector: FalsingCollector, 74 dumpManager: DumpManager 75 ) : Gefingerpoken, Dumpable { 76 companion object { 77 private val SPRING_BACK_ANIMATION_LENGTH_MS = 375 78 } 79 private val mPowerManager: PowerManager? 80 81 private var mInitialTouchX: Float = 0.0f 82 private var mInitialTouchY: Float = 0.0f 83 var isExpanding: Boolean = false 84 private set(value) { 85 val changed = field != value 86 field = value 87 bypassController.isPulseExpanding = value 88 if (changed) { 89 if (value) { 90 lockscreenShadeTransitionController.onPulseExpansionStarted() 91 } else { 92 if (!leavingLockscreen) { 93 bypassController.maybePerformPendingUnlock() 94 pulseExpandAbortListener?.run() 95 } 96 } 97 headsUpManager.unpinAll(true /* userUnPinned */) 98 } 99 } 100 var leavingLockscreen: Boolean = false 101 private set 102 private var touchSlop = 0f 103 private var minDragDistance = 0 104 private lateinit var stackScrollerController: NotificationStackScrollLayoutController 105 private val mTemp2 = IntArray(2) 106 private var mDraggedFarEnough: Boolean = false 107 private var mStartingChild: ExpandableView? = null 108 private var mPulsing: Boolean = false 109 110 private var velocityTracker: VelocityTracker? = null 111 112 private val isFalseTouch: Boolean 113 get() = falsingManager.isFalseTouch(NOTIFICATION_DRAG_DOWN) 114 var qsExpanded: Boolean = false 115 var pulseExpandAbortListener: Runnable? = null 116 var bouncerShowing: Boolean = false 117 118 init { 119 initResources(context) 120 configurationController.addCallback(object : ConfigurationController.ConfigurationListener { 121 override fun onConfigChanged(newConfig: Configuration?) { 122 initResources(context) 123 } 124 }) 125 126 shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> 127 if (qsExpanded != isQsExpanded) { 128 qsExpanded = isQsExpanded 129 } 130 } 131 132 mPowerManager = context.getSystemService(PowerManager::class.java) 133 dumpManager.registerDumpable(this) 134 } 135 136 private fun initResources(context: Context) { 137 minDragDistance = context.resources.getDimensionPixelSize( 138 R.dimen.keyguard_drag_down_min_distance) 139 touchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat() 140 } 141 142 override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 143 return canHandleMotionEvent() && startExpansion(event) 144 } 145 146 private fun canHandleMotionEvent(): Boolean { 147 return wakeUpCoordinator.canShowPulsingHuns && !qsExpanded && !bouncerShowing 148 } 149 150 private fun startExpansion(event: MotionEvent): Boolean { 151 if (velocityTracker == null) { 152 velocityTracker = VelocityTracker.obtain() 153 } 154 velocityTracker!!.addMovement(event) 155 val x = event.x 156 val y = event.y 157 158 when (event.actionMasked) { 159 MotionEvent.ACTION_DOWN -> { 160 mDraggedFarEnough = false 161 isExpanding = false 162 leavingLockscreen = false 163 mStartingChild = null 164 mInitialTouchY = y 165 mInitialTouchX = x 166 } 167 168 MotionEvent.ACTION_MOVE -> { 169 val h = y - mInitialTouchY 170 if (h > touchSlop && h > Math.abs(x - mInitialTouchX)) { 171 falsingCollector.onStartExpandingFromPulse() 172 isExpanding = true 173 captureStartingChild(mInitialTouchX, mInitialTouchY) 174 mInitialTouchY = y 175 mInitialTouchX = x 176 return true 177 } 178 } 179 180 MotionEvent.ACTION_UP -> { 181 recycleVelocityTracker() 182 isExpanding = false 183 } 184 185 MotionEvent.ACTION_CANCEL -> { 186 recycleVelocityTracker() 187 isExpanding = false 188 } 189 } 190 return false 191 } 192 193 private fun recycleVelocityTracker() { 194 velocityTracker?.recycle() 195 velocityTracker = null 196 } 197 198 override fun onTouchEvent(event: MotionEvent): Boolean { 199 val finishExpanding = (event.action == MotionEvent.ACTION_CANCEL || 200 event.action == MotionEvent.ACTION_UP) && isExpanding 201 202 val isDraggingNotificationOrCanBypass = mStartingChild?.showingPulsing() == true || 203 bypassController.canBypass() 204 if ((!canHandleMotionEvent() || !isDraggingNotificationOrCanBypass) && !finishExpanding) { 205 // We allow cancellations/finishing to still go through here to clean up the state 206 return false 207 } 208 209 if (velocityTracker == null || !isExpanding || 210 event.actionMasked == MotionEvent.ACTION_DOWN) { 211 return startExpansion(event) 212 } 213 velocityTracker!!.addMovement(event) 214 val y = event.y 215 216 val moveDistance = y - mInitialTouchY 217 when (event.actionMasked) { 218 MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance) 219 MotionEvent.ACTION_UP -> { 220 velocityTracker!!.computeCurrentVelocity(1000 /* units */) 221 val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000 && 222 statusBarStateController.state != StatusBarState.SHADE 223 if (!falsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) { 224 finishExpansion() 225 } else { 226 cancelExpansion() 227 } 228 recycleVelocityTracker() 229 } 230 MotionEvent.ACTION_CANCEL -> { 231 cancelExpansion() 232 recycleVelocityTracker() 233 } 234 } 235 return isExpanding 236 } 237 238 private fun finishExpansion() { 239 val startingChild = mStartingChild 240 if (mStartingChild != null) { 241 setUserLocked(mStartingChild!!, false) 242 mStartingChild = null 243 } 244 if (statusBarStateController.isDozing) { 245 wakeUpCoordinator.willWakeUp = true 246 mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, 247 "com.android.systemui:PULSEDRAG") 248 } 249 lockscreenShadeTransitionController.goToLockedShade(startingChild, 250 needsQSAnimation = false) 251 lockscreenShadeTransitionController.finishPulseAnimation(cancelled = false) 252 leavingLockscreen = true 253 isExpanding = false 254 if (mStartingChild is ExpandableNotificationRow) { 255 val row = mStartingChild as ExpandableNotificationRow? 256 row!!.onExpandedByGesture(true /* userExpanded */) 257 } 258 } 259 260 private fun updateExpansionHeight(height: Float) { 261 var expansionHeight = max(height, 0.0f) 262 if (mStartingChild != null) { 263 val child = mStartingChild!! 264 val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(), 265 child.maxContentHeight) 266 child.actualHeight = newHeight 267 } else { 268 wakeUpCoordinator.setNotificationsVisibleForExpansion( 269 height 270 > lockscreenShadeTransitionController.distanceUntilShowingPulsingNotifications, 271 true /* animate */, 272 true /* increaseSpeed */) 273 } 274 lockscreenShadeTransitionController.setPulseHeight(expansionHeight, animate = false) 275 } 276 277 private fun captureStartingChild(x: Float, y: Float) { 278 if (mStartingChild == null && !bypassController.bypassEnabled) { 279 mStartingChild = findView(x, y) 280 if (mStartingChild != null) { 281 setUserLocked(mStartingChild!!, true) 282 } 283 } 284 } 285 286 @VisibleForTesting 287 fun reset( 288 child: ExpandableView, 289 animationDuration: Long = SPRING_BACK_ANIMATION_LENGTH_MS.toLong() 290 ) { 291 if (child.actualHeight == child.collapsedHeight) { 292 setUserLocked(child, false) 293 return 294 } 295 val anim = ValueAnimator.ofInt(child.actualHeight, child.collapsedHeight) 296 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN 297 anim.duration = animationDuration 298 anim.addUpdateListener { animation: ValueAnimator -> 299 // don't use reflection, because the `actualHeight` field may be obfuscated 300 child.actualHeight = animation.animatedValue as Int 301 } 302 anim.addListener(object : AnimatorListenerAdapter() { 303 override fun onAnimationEnd(animation: Animator) { 304 setUserLocked(child, false) 305 } 306 }) 307 anim.start() 308 } 309 310 private fun setUserLocked(child: ExpandableView, userLocked: Boolean) { 311 if (child is ExpandableNotificationRow) { 312 child.isUserLocked = userLocked 313 } 314 } 315 316 private fun cancelExpansion() { 317 isExpanding = false 318 falsingCollector.onExpansionFromPulseStopped() 319 if (mStartingChild != null) { 320 reset(mStartingChild!!) 321 mStartingChild = null 322 } 323 lockscreenShadeTransitionController.finishPulseAnimation(cancelled = true) 324 wakeUpCoordinator.setNotificationsVisibleForExpansion(false /* visible */, 325 true /* animate */, 326 false /* increaseSpeed */) 327 } 328 329 private fun findView(x: Float, y: Float): ExpandableView? { 330 var totalX = x 331 var totalY = y 332 stackScrollerController.getLocationOnScreen(mTemp2) 333 totalX += mTemp2[0].toFloat() 334 totalY += mTemp2[1].toFloat() 335 val childAtRawPosition = stackScrollerController.getChildAtRawPosition(totalX, totalY) 336 return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) { 337 childAtRawPosition 338 } else null 339 } 340 341 fun setUp(stackScrollerController: NotificationStackScrollLayoutController) { 342 this.stackScrollerController = stackScrollerController 343 } 344 345 fun setPulsing(pulsing: Boolean) { 346 mPulsing = pulsing 347 } 348 349 override fun dump(pw: PrintWriter, args: Array<out String>) { 350 IndentingPrintWriter(pw, " ").let { 351 it.println("PulseExpansionHandler:") 352 it.increaseIndent() 353 it.println("isExpanding: $isExpanding") 354 it.println("leavingLockscreen: $leavingLockscreen") 355 it.println("mPulsing: $mPulsing") 356 it.println("qsExpanded: $qsExpanded") 357 it.println("bouncerShowing: $bouncerShowing") 358 } 359 } 360 } 361