1 /* <lambda>null2 * 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.systemui.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 val topEntry = headsUpManager.topEntry 91 topEntry?.let { 92 roundnessManager.setTrackingHeadsUp(it.row) 93 } 94 lockscreenShadeTransitionController.onPulseExpansionStarted() 95 } else { 96 roundnessManager.setTrackingHeadsUp(null) 97 if (!leavingLockscreen) { 98 bypassController.maybePerformPendingUnlock() 99 pulseExpandAbortListener?.run() 100 } 101 } 102 headsUpManager.unpinAll(true /* userUnPinned */) 103 } 104 } 105 var leavingLockscreen: Boolean = false 106 private set 107 private var touchSlop = 0f 108 private var minDragDistance = 0 109 private lateinit var stackScrollerController: NotificationStackScrollLayoutController 110 private val mTemp2 = IntArray(2) 111 private var mDraggedFarEnough: Boolean = false 112 private var mStartingChild: ExpandableView? = null 113 private var mPulsing: Boolean = false 114 115 private var velocityTracker: VelocityTracker? = null 116 117 private val isFalseTouch: Boolean 118 get() = falsingManager.isFalseTouch(NOTIFICATION_DRAG_DOWN) 119 var qsExpanded: Boolean = false 120 var pulseExpandAbortListener: Runnable? = null 121 var bouncerShowing: Boolean = false 122 123 init { 124 initResources(context) 125 configurationController.addCallback(object : ConfigurationController.ConfigurationListener { 126 override fun onConfigChanged(newConfig: Configuration?) { 127 initResources(context) 128 } 129 }) 130 131 shadeExpansionStateManager.addQsExpansionListener { isQsExpanded -> 132 if (qsExpanded != isQsExpanded) { 133 qsExpanded = isQsExpanded 134 } 135 } 136 137 mPowerManager = context.getSystemService(PowerManager::class.java) 138 dumpManager.registerDumpable(this) 139 } 140 141 private fun initResources(context: Context) { 142 minDragDistance = context.resources.getDimensionPixelSize( 143 R.dimen.keyguard_drag_down_min_distance) 144 touchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat() 145 } 146 147 override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 148 return canHandleMotionEvent() && startExpansion(event) 149 } 150 151 private fun canHandleMotionEvent(): Boolean { 152 return wakeUpCoordinator.canShowPulsingHuns && !qsExpanded && !bouncerShowing 153 } 154 155 private fun startExpansion(event: MotionEvent): Boolean { 156 if (velocityTracker == null) { 157 velocityTracker = VelocityTracker.obtain() 158 } 159 velocityTracker!!.addMovement(event) 160 val x = event.x 161 val y = event.y 162 163 when (event.actionMasked) { 164 MotionEvent.ACTION_DOWN -> { 165 mDraggedFarEnough = false 166 isExpanding = false 167 leavingLockscreen = false 168 mStartingChild = null 169 mInitialTouchY = y 170 mInitialTouchX = x 171 } 172 173 MotionEvent.ACTION_MOVE -> { 174 val h = y - mInitialTouchY 175 if (h > touchSlop && h > Math.abs(x - mInitialTouchX)) { 176 falsingCollector.onStartExpandingFromPulse() 177 isExpanding = true 178 captureStartingChild(mInitialTouchX, mInitialTouchY) 179 mInitialTouchY = y 180 mInitialTouchX = x 181 return true 182 } 183 } 184 185 MotionEvent.ACTION_UP -> { 186 recycleVelocityTracker() 187 isExpanding = false 188 } 189 190 MotionEvent.ACTION_CANCEL -> { 191 recycleVelocityTracker() 192 isExpanding = false 193 } 194 } 195 return false 196 } 197 198 private fun recycleVelocityTracker() { 199 velocityTracker?.recycle() 200 velocityTracker = null 201 } 202 203 override fun onTouchEvent(event: MotionEvent): Boolean { 204 val finishExpanding = (event.action == MotionEvent.ACTION_CANCEL || 205 event.action == MotionEvent.ACTION_UP) && isExpanding 206 207 val isDraggingNotificationOrCanBypass = mStartingChild?.showingPulsing() == true || 208 bypassController.canBypass() 209 if ((!canHandleMotionEvent() || !isDraggingNotificationOrCanBypass) && !finishExpanding) { 210 // We allow cancellations/finishing to still go through here to clean up the state 211 return false 212 } 213 214 if (velocityTracker == null || !isExpanding || 215 event.actionMasked == MotionEvent.ACTION_DOWN) { 216 return startExpansion(event) 217 } 218 velocityTracker!!.addMovement(event) 219 val y = event.y 220 221 val moveDistance = y - mInitialTouchY 222 when (event.actionMasked) { 223 MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance) 224 MotionEvent.ACTION_UP -> { 225 velocityTracker!!.computeCurrentVelocity(1000 /* units */) 226 val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000 && 227 statusBarStateController.state != StatusBarState.SHADE 228 if (!falsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) { 229 finishExpansion() 230 } else { 231 cancelExpansion() 232 } 233 recycleVelocityTracker() 234 } 235 MotionEvent.ACTION_CANCEL -> { 236 cancelExpansion() 237 recycleVelocityTracker() 238 } 239 } 240 return isExpanding 241 } 242 243 private fun finishExpansion() { 244 val startingChild = mStartingChild 245 if (mStartingChild != null) { 246 setUserLocked(mStartingChild!!, false) 247 mStartingChild = null 248 } 249 if (statusBarStateController.isDozing) { 250 wakeUpCoordinator.willWakeUp = true 251 mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, 252 "com.android.systemui:PULSEDRAG") 253 } 254 lockscreenShadeTransitionController.goToLockedShade(startingChild, 255 needsQSAnimation = false) 256 lockscreenShadeTransitionController.finishPulseAnimation(cancelled = false) 257 leavingLockscreen = true 258 isExpanding = false 259 if (mStartingChild is ExpandableNotificationRow) { 260 val row = mStartingChild as ExpandableNotificationRow? 261 row!!.onExpandedByGesture(true /* userExpanded */) 262 } 263 } 264 265 private fun updateExpansionHeight(height: Float) { 266 var expansionHeight = max(height, 0.0f) 267 if (mStartingChild != null) { 268 val child = mStartingChild!! 269 val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(), 270 child.maxContentHeight) 271 child.actualHeight = newHeight 272 } else { 273 wakeUpCoordinator.setNotificationsVisibleForExpansion( 274 height 275 > lockscreenShadeTransitionController.distanceUntilShowingPulsingNotifications, 276 true /* animate */, 277 true /* increaseSpeed */) 278 } 279 lockscreenShadeTransitionController.setPulseHeight(expansionHeight, animate = false) 280 } 281 282 private fun captureStartingChild(x: Float, y: Float) { 283 if (mStartingChild == null && !bypassController.bypassEnabled) { 284 mStartingChild = findView(x, y) 285 if (mStartingChild != null) { 286 setUserLocked(mStartingChild!!, true) 287 } 288 } 289 } 290 291 @VisibleForTesting 292 fun reset( 293 child: ExpandableView, 294 animationDuration: Long = SPRING_BACK_ANIMATION_LENGTH_MS.toLong() 295 ) { 296 if (child.actualHeight == child.collapsedHeight) { 297 setUserLocked(child, false) 298 return 299 } 300 val anim = ValueAnimator.ofInt(child.actualHeight, child.collapsedHeight) 301 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN 302 anim.duration = animationDuration 303 anim.addUpdateListener { animation: ValueAnimator -> 304 // don't use reflection, because the `actualHeight` field may be obfuscated 305 child.actualHeight = animation.animatedValue as Int 306 } 307 anim.addListener(object : AnimatorListenerAdapter() { 308 override fun onAnimationEnd(animation: Animator) { 309 setUserLocked(child, false) 310 } 311 }) 312 anim.start() 313 } 314 315 private fun setUserLocked(child: ExpandableView, userLocked: Boolean) { 316 if (child is ExpandableNotificationRow) { 317 child.isUserLocked = userLocked 318 } 319 } 320 321 private fun cancelExpansion() { 322 isExpanding = false 323 falsingCollector.onExpansionFromPulseStopped() 324 if (mStartingChild != null) { 325 reset(mStartingChild!!) 326 mStartingChild = null 327 } 328 lockscreenShadeTransitionController.finishPulseAnimation(cancelled = true) 329 wakeUpCoordinator.setNotificationsVisibleForExpansion(false /* visible */, 330 true /* animate */, 331 false /* increaseSpeed */) 332 } 333 334 private fun findView(x: Float, y: Float): ExpandableView? { 335 var totalX = x 336 var totalY = y 337 stackScrollerController.getLocationOnScreen(mTemp2) 338 totalX += mTemp2[0].toFloat() 339 totalY += mTemp2[1].toFloat() 340 val childAtRawPosition = stackScrollerController.getChildAtRawPosition(totalX, totalY) 341 return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) { 342 childAtRawPosition 343 } else null 344 } 345 346 fun setUp(stackScrollerController: NotificationStackScrollLayoutController) { 347 this.stackScrollerController = stackScrollerController 348 } 349 350 fun setPulsing(pulsing: Boolean) { 351 mPulsing = pulsing 352 } 353 354 override fun dump(pw: PrintWriter, args: Array<out String>) { 355 IndentingPrintWriter(pw, " ").let { 356 it.println("PulseExpansionHandler:") 357 it.increaseIndent() 358 it.println("isExpanding: $isExpanding") 359 it.println("leavingLockscreen: $leavingLockscreen") 360 it.println("mPulsing: $mPulsing") 361 it.println("qsExpanded: $qsExpanded") 362 it.println("bouncerShowing: $bouncerShowing") 363 } 364 } 365 } 366