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