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