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