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