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.ObjectAnimator 22 import android.content.Context 23 import android.content.res.Configuration 24 import android.os.PowerManager 25 import android.os.PowerManager.WAKE_REASON_GESTURE 26 import android.os.SystemClock 27 import android.view.MotionEvent 28 import android.view.VelocityTracker 29 import android.view.ViewConfiguration 30 import com.android.systemui.Gefingerpoken 31 import com.android.systemui.R 32 import com.android.systemui.animation.Interpolators 33 import com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN 34 import com.android.systemui.classifier.FalsingCollector 35 import com.android.systemui.dagger.SysUISingleton 36 import com.android.systemui.plugins.FalsingManager 37 import com.android.systemui.plugins.statusbar.StatusBarStateController 38 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator 39 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 40 import com.android.systemui.statusbar.notification.row.ExpandableView 41 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager 42 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 43 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone 44 import com.android.systemui.statusbar.phone.KeyguardBypassController 45 import com.android.systemui.statusbar.policy.ConfigurationController 46 import javax.inject.Inject 47 import kotlin.math.max 48 49 /** 50 * A utility class to enable the downward swipe on when pulsing. 51 */ 52 @SysUISingleton 53 class PulseExpansionHandler @Inject 54 constructor( 55 context: Context, 56 private val wakeUpCoordinator: NotificationWakeUpCoordinator, 57 private val bypassController: KeyguardBypassController, 58 private val headsUpManager: HeadsUpManagerPhone, 59 private val roundnessManager: NotificationRoundnessManager, 60 private val configurationController: ConfigurationController, 61 private val statusBarStateController: StatusBarStateController, 62 private val falsingManager: FalsingManager, 63 private val lockscreenShadeTransitionController: LockscreenShadeTransitionController, 64 private val falsingCollector: FalsingCollector 65 ) : Gefingerpoken { 66 companion object { 67 private val SPRING_BACK_ANIMATION_LENGTH_MS = 375 68 } 69 private val mPowerManager: PowerManager? 70 71 private var mInitialTouchX: Float = 0.0f 72 private var mInitialTouchY: Float = 0.0f 73 var isExpanding: Boolean = false 74 private set(value) { 75 val changed = field != value 76 field = value 77 bypassController.isPulseExpanding = value 78 if (changed) { 79 if (value) { 80 val topEntry = headsUpManager.topEntry <lambda>null81 topEntry?.let { 82 roundnessManager.setTrackingHeadsUp(it.row) 83 } 84 lockscreenShadeTransitionController.onPulseExpansionStarted() 85 } else { 86 roundnessManager.setTrackingHeadsUp(null) 87 if (!leavingLockscreen) { 88 bypassController.maybePerformPendingUnlock() 89 pulseExpandAbortListener?.run() 90 } 91 } 92 headsUpManager.unpinAll(true /* userUnPinned */) 93 } 94 } 95 var leavingLockscreen: Boolean = false 96 private set 97 private var touchSlop = 0f 98 private var minDragDistance = 0 99 private lateinit var stackScrollerController: NotificationStackScrollLayoutController 100 private val mTemp2 = IntArray(2) 101 private var mDraggedFarEnough: Boolean = false 102 private var mStartingChild: ExpandableView? = null 103 private var mPulsing: Boolean = false 104 var isWakingToShadeLocked: Boolean = false 105 private set 106 107 private var velocityTracker: VelocityTracker? = null 108 109 private val isFalseTouch: Boolean 110 get() = falsingManager.isFalseTouch(NOTIFICATION_DRAG_DOWN) 111 var qsExpanded: Boolean = false 112 var pulseExpandAbortListener: Runnable? = null 113 var bouncerShowing: Boolean = false 114 115 init { 116 initResources(context) 117 configurationController.addCallback(object : ConfigurationController.ConfigurationListener { onConfigChangednull118 override fun onConfigChanged(newConfig: Configuration?) { 119 initResources(context) 120 } 121 }) 122 mPowerManager = context.getSystemService(PowerManager::class.java) 123 } 124 initResourcesnull125 private fun initResources(context: Context) { 126 minDragDistance = context.resources.getDimensionPixelSize( 127 R.dimen.keyguard_drag_down_min_distance) 128 touchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat() 129 } 130 onInterceptTouchEventnull131 override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 132 return canHandleMotionEvent() && startExpansion(event) 133 } 134 canHandleMotionEventnull135 private fun canHandleMotionEvent(): Boolean { 136 return wakeUpCoordinator.canShowPulsingHuns && !qsExpanded && !bouncerShowing 137 } 138 startExpansionnull139 private fun startExpansion(event: MotionEvent): Boolean { 140 if (velocityTracker == null) { 141 velocityTracker = VelocityTracker.obtain() 142 } 143 velocityTracker!!.addMovement(event) 144 val x = event.x 145 val y = event.y 146 147 when (event.actionMasked) { 148 MotionEvent.ACTION_DOWN -> { 149 mDraggedFarEnough = false 150 isExpanding = false 151 leavingLockscreen = false 152 mStartingChild = null 153 mInitialTouchY = y 154 mInitialTouchX = x 155 } 156 157 MotionEvent.ACTION_MOVE -> { 158 val h = y - mInitialTouchY 159 if (h > touchSlop && h > Math.abs(x - mInitialTouchX)) { 160 falsingCollector.onStartExpandingFromPulse() 161 isExpanding = true 162 captureStartingChild(mInitialTouchX, mInitialTouchY) 163 mInitialTouchY = y 164 mInitialTouchX = x 165 return true 166 } 167 } 168 169 MotionEvent.ACTION_UP -> { 170 recycleVelocityTracker() 171 isExpanding = false 172 } 173 174 MotionEvent.ACTION_CANCEL -> { 175 recycleVelocityTracker() 176 isExpanding = false 177 } 178 } 179 return false 180 } 181 recycleVelocityTrackernull182 private fun recycleVelocityTracker() { 183 velocityTracker?.recycle() 184 velocityTracker = null 185 } 186 onTouchEventnull187 override fun onTouchEvent(event: MotionEvent): Boolean { 188 val finishExpanding = (event.action == MotionEvent.ACTION_CANCEL || 189 event.action == MotionEvent.ACTION_UP) && isExpanding 190 if (!canHandleMotionEvent() && !finishExpanding) { 191 // We allow cancellations/finishing to still go through here to clean up the state 192 return false 193 } 194 195 if (velocityTracker == null || !isExpanding || 196 event.actionMasked == MotionEvent.ACTION_DOWN) { 197 return startExpansion(event) 198 } 199 velocityTracker!!.addMovement(event) 200 val y = event.y 201 202 val moveDistance = y - mInitialTouchY 203 when (event.actionMasked) { 204 MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance) 205 MotionEvent.ACTION_UP -> { 206 velocityTracker!!.computeCurrentVelocity(1000 /* units */) 207 val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000 && 208 statusBarStateController.state != StatusBarState.SHADE 209 if (!falsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) { 210 finishExpansion() 211 } else { 212 cancelExpansion() 213 } 214 recycleVelocityTracker() 215 } 216 MotionEvent.ACTION_CANCEL -> { 217 cancelExpansion() 218 recycleVelocityTracker() 219 } 220 } 221 return isExpanding 222 } 223 finishExpansionnull224 private fun finishExpansion() { 225 val startingChild = mStartingChild 226 if (mStartingChild != null) { 227 setUserLocked(mStartingChild!!, false) 228 mStartingChild = null 229 } 230 if (statusBarStateController.isDozing) { 231 isWakingToShadeLocked = true 232 wakeUpCoordinator.willWakeUp = true 233 mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE, 234 "com.android.systemui:PULSEDRAG") 235 } 236 lockscreenShadeTransitionController.goToLockedShade(startingChild, 237 needsQSAnimation = false) 238 lockscreenShadeTransitionController.finishPulseAnimation(cancelled = false) 239 leavingLockscreen = true 240 isExpanding = false 241 if (mStartingChild is ExpandableNotificationRow) { 242 val row = mStartingChild as ExpandableNotificationRow? 243 row!!.onExpandedByGesture(true /* userExpanded */) 244 } 245 } 246 updateExpansionHeightnull247 private fun updateExpansionHeight(height: Float) { 248 var expansionHeight = max(height, 0.0f) 249 if (mStartingChild != null) { 250 val child = mStartingChild!! 251 val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(), 252 child.maxContentHeight) 253 child.actualHeight = newHeight 254 } else { 255 wakeUpCoordinator.setNotificationsVisibleForExpansion( 256 height 257 > lockscreenShadeTransitionController.distanceUntilShowingPulsingNotifications, 258 true /* animate */, 259 true /* increaseSpeed */) 260 } 261 lockscreenShadeTransitionController.setPulseHeight(expansionHeight, animate = false) 262 } 263 captureStartingChildnull264 private fun captureStartingChild(x: Float, y: Float) { 265 if (mStartingChild == null && !bypassController.bypassEnabled) { 266 mStartingChild = findView(x, y) 267 if (mStartingChild != null) { 268 setUserLocked(mStartingChild!!, true) 269 } 270 } 271 } 272 resetnull273 private fun reset(child: ExpandableView) { 274 if (child.actualHeight == child.collapsedHeight) { 275 setUserLocked(child, false) 276 return 277 } 278 val anim = ObjectAnimator.ofInt(child, "actualHeight", 279 child.actualHeight, child.collapsedHeight) 280 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN 281 anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong() 282 anim.addListener(object : AnimatorListenerAdapter() { 283 override fun onAnimationEnd(animation: Animator) { 284 setUserLocked(child, false) 285 } 286 }) 287 anim.start() 288 } 289 setUserLockednull290 private fun setUserLocked(child: ExpandableView, userLocked: Boolean) { 291 if (child is ExpandableNotificationRow) { 292 child.isUserLocked = userLocked 293 } 294 } 295 cancelExpansionnull296 private fun cancelExpansion() { 297 isExpanding = false 298 falsingCollector.onExpansionFromPulseStopped() 299 if (mStartingChild != null) { 300 reset(mStartingChild!!) 301 mStartingChild = null 302 } 303 lockscreenShadeTransitionController.finishPulseAnimation(cancelled = true) 304 wakeUpCoordinator.setNotificationsVisibleForExpansion(false /* visible */, 305 true /* animate */, 306 false /* increaseSpeed */) 307 } 308 findViewnull309 private fun findView(x: Float, y: Float): ExpandableView? { 310 var totalX = x 311 var totalY = y 312 stackScrollerController.getLocationOnScreen(mTemp2) 313 totalX += mTemp2[0].toFloat() 314 totalY += mTemp2[1].toFloat() 315 val childAtRawPosition = stackScrollerController.getChildAtRawPosition(totalX, totalY) 316 return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) { 317 childAtRawPosition 318 } else null 319 } 320 setUpnull321 fun setUp(stackScrollerController: NotificationStackScrollLayoutController) { 322 this.stackScrollerController = stackScrollerController 323 } 324 setPulsingnull325 fun setPulsing(pulsing: Boolean) { 326 mPulsing = pulsing 327 } 328 onStartedWakingUpnull329 fun onStartedWakingUp() { 330 isWakingToShadeLocked = false 331 } 332 } 333