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.ObjectAnimator
22 import android.animation.ValueAnimator
23 import android.content.Context
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.Interpolators
32 import com.android.systemui.R
33 import com.android.systemui.plugins.FalsingManager
34 import com.android.systemui.plugins.statusbar.StatusBarStateController
35 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
37 import com.android.systemui.statusbar.notification.row.ExpandableView
38 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager
39 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
40 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
41 import com.android.systemui.statusbar.phone.KeyguardBypassController
42 import com.android.systemui.statusbar.phone.ShadeController
43 import javax.inject.Inject
44 import javax.inject.Singleton
45 import kotlin.math.max
46 
47 /**
48  * A utility class to enable the downward swipe on when pulsing.
49  */
50 @Singleton
51 class PulseExpansionHandler @Inject
52 constructor(
53     context: Context,
54     private val wakeUpCoordinator: NotificationWakeUpCoordinator,
55     private val bypassController: KeyguardBypassController,
56     private val headsUpManager: HeadsUpManagerPhone,
57     private val roundnessManager: NotificationRoundnessManager,
58     private val statusBarStateController: StatusBarStateController,
59     private val falsingManager: FalsingManager
60 ) : Gefingerpoken {
61     companion object {
62         private val RUBBERBAND_FACTOR_STATIC = 0.25f
63         private val SPRING_BACK_ANIMATION_LENGTH_MS = 375
64     }
65     private val mPowerManager: PowerManager?
66     private lateinit var shadeController: ShadeController
67 
68     private val mMinDragDistance: Int
69     private var mInitialTouchX: Float = 0.0f
70     private var mInitialTouchY: Float = 0.0f
71     var isExpanding: Boolean = false
72         private set(value) {
73             val changed = field != value
74             field = value
75             bypassController.isPulseExpanding = value
76             if (changed) {
77                 if (value) {
78                     val topEntry = headsUpManager.topEntry
79                     topEntry?.let {
80                         roundnessManager.setTrackingHeadsUp(it.row)
81                     }
82                 } else {
83                     roundnessManager.setTrackingHeadsUp(null)
84                     if (!leavingLockscreen) {
85                         bypassController.maybePerformPendingUnlock()
86                         pulseExpandAbortListener?.run()
87                     }
88                 }
89                 headsUpManager.unpinAll(true /* userUnPinned */)
90             }
91         }
92     var leavingLockscreen: Boolean = false
93         private set
94     private val mTouchSlop: Float
95     private lateinit var expansionCallback: ExpansionCallback
96     private lateinit var stackScroller: NotificationStackScrollLayout
97     private val mTemp2 = IntArray(2)
98     private var mDraggedFarEnough: Boolean = false
99     private var mStartingChild: ExpandableView? = null
100     private var mPulsing: Boolean = false
101     var isWakingToShadeLocked: Boolean = false
102         private set
103     private var mEmptyDragAmount: Float = 0.0f
104     private var mWakeUpHeight: Float = 0.0f
105     private var mReachedWakeUpHeight: Boolean = false
106     private var velocityTracker: VelocityTracker? = null
107 
108     private val isFalseTouch: Boolean
109         get() = falsingManager.isFalseTouch
110     var qsExpanded: Boolean = false
111     var pulseExpandAbortListener: Runnable? = null
112     var bouncerShowing: Boolean = false
113 
114     init {
115         mMinDragDistance = context.resources.getDimensionPixelSize(
116                 R.dimen.keyguard_drag_down_min_distance)
117         mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
118         mPowerManager = context.getSystemService(PowerManager::class.java)
119     }
120 
121     override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
122         return canHandleMotionEvent() && startExpansion(event)
123     }
124 
125     private fun canHandleMotionEvent(): Boolean {
126         return wakeUpCoordinator.canShowPulsingHuns && !qsExpanded && !bouncerShowing
127     }
128 
129     private fun startExpansion(event: MotionEvent): Boolean {
130         if (velocityTracker == null) {
131             velocityTracker = VelocityTracker.obtain()
132         }
133         velocityTracker!!.addMovement(event)
134         val x = event.x
135         val y = event.y
136 
137         when (event.actionMasked) {
138             MotionEvent.ACTION_DOWN -> {
139                 mDraggedFarEnough = false
140                 isExpanding = false
141                 leavingLockscreen = false
142                 mStartingChild = null
143                 mInitialTouchY = y
144                 mInitialTouchX = x
145             }
146 
147             MotionEvent.ACTION_MOVE -> {
148                 val h = y - mInitialTouchY
149                 if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
150                     falsingManager.onStartExpandingFromPulse()
151                     isExpanding = true
152                     captureStartingChild(mInitialTouchX, mInitialTouchY)
153                     mInitialTouchY = y
154                     mInitialTouchX = x
155                     mWakeUpHeight = wakeUpCoordinator.getWakeUpHeight()
156                     mReachedWakeUpHeight = false
157                     return true
158                 }
159             }
160 
161             MotionEvent.ACTION_UP -> {
162                 recycleVelocityTracker()
163                 isExpanding = false
164             }
165 
166             MotionEvent.ACTION_CANCEL -> {
167                 recycleVelocityTracker()
168                 isExpanding = false
169             }
170         }
171         return false
172     }
173 
174     private fun recycleVelocityTracker() {
175         velocityTracker?.recycle()
176         velocityTracker = null
177     }
178 
179     override fun onTouchEvent(event: MotionEvent): Boolean {
180         if (!canHandleMotionEvent()) {
181             return false
182         }
183 
184         if (velocityTracker == null || !isExpanding ||
185                 event.actionMasked == MotionEvent.ACTION_DOWN) {
186             return startExpansion(event)
187         }
188         velocityTracker!!.addMovement(event)
189         val y = event.y
190 
191         val moveDistance = y - mInitialTouchY
192         when (event.actionMasked) {
193             MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance)
194             MotionEvent.ACTION_UP -> {
195                 velocityTracker!!.computeCurrentVelocity(1000 /* units */)
196                 val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000 &&
197                         statusBarStateController.state != StatusBarState.SHADE
198                 if (!falsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) {
199                     finishExpansion()
200                 } else {
201                     cancelExpansion()
202                 }
203                 recycleVelocityTracker()
204             }
205             MotionEvent.ACTION_CANCEL -> {
206                 cancelExpansion()
207                 recycleVelocityTracker()
208             }
209         }
210         return isExpanding
211     }
212 
213     private fun finishExpansion() {
214         resetClock()
215         if (mStartingChild != null) {
216             setUserLocked(mStartingChild!!, false)
217             mStartingChild = null
218         }
219         if (statusBarStateController.isDozing) {
220             isWakingToShadeLocked = true
221             wakeUpCoordinator.willWakeUp = true
222             mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE,
223                     "com.android.systemui:PULSEDRAG")
224         }
225         shadeController.goToLockedShade(mStartingChild)
226         leavingLockscreen = true
227         isExpanding = false
228         if (mStartingChild is ExpandableNotificationRow) {
229             val row = mStartingChild as ExpandableNotificationRow?
230             row!!.onExpandedByGesture(true /* userExpanded */)
231         }
232     }
233 
234     private fun updateExpansionHeight(height: Float) {
235         var expansionHeight = max(height, 0.0f)
236         if (!mReachedWakeUpHeight && height > mWakeUpHeight) {
237             mReachedWakeUpHeight = true
238         }
239         if (mStartingChild != null) {
240             val child = mStartingChild!!
241             val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(),
242                     child.maxContentHeight)
243             child.actualHeight = newHeight
244             expansionHeight = max(newHeight.toFloat(), expansionHeight)
245         } else {
246             val target = if (mReachedWakeUpHeight) mWakeUpHeight else 0.0f
247             wakeUpCoordinator.setNotificationsVisibleForExpansion(height > target,
248                     true /* animate */,
249                     true /* increaseSpeed */)
250             expansionHeight = max(mWakeUpHeight, expansionHeight)
251         }
252         val emptyDragAmount = wakeUpCoordinator.setPulseHeight(expansionHeight)
253         setEmptyDragAmount(emptyDragAmount * RUBBERBAND_FACTOR_STATIC)
254     }
255 
256     private fun captureStartingChild(x: Float, y: Float) {
257         if (mStartingChild == null && !bypassController.bypassEnabled) {
258             mStartingChild = findView(x, y)
259             if (mStartingChild != null) {
260                 setUserLocked(mStartingChild!!, true)
261             }
262         }
263     }
264 
265     private fun setEmptyDragAmount(amount: Float) {
266         mEmptyDragAmount = amount
267         expansionCallback.setEmptyDragAmount(amount)
268     }
269 
270     private fun reset(child: ExpandableView) {
271         if (child.actualHeight == child.collapsedHeight) {
272             setUserLocked(child, false)
273             return
274         }
275         val anim = ObjectAnimator.ofInt(child, "actualHeight",
276                 child.actualHeight, child.collapsedHeight)
277         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
278         anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
279         anim.addListener(object : AnimatorListenerAdapter() {
280             override fun onAnimationEnd(animation: Animator) {
281                 setUserLocked(child, false)
282             }
283         })
284         anim.start()
285     }
286 
287     private fun setUserLocked(child: ExpandableView, userLocked: Boolean) {
288         if (child is ExpandableNotificationRow) {
289             child.isUserLocked = userLocked
290         }
291     }
292 
293     private fun resetClock() {
294         val anim = ValueAnimator.ofFloat(mEmptyDragAmount, 0f)
295         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
296         anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
297         anim.addUpdateListener { animation -> setEmptyDragAmount(animation.animatedValue as Float) }
298         anim.start()
299     }
300 
301     private fun cancelExpansion() {
302         isExpanding = false
303         falsingManager.onExpansionFromPulseStopped()
304         if (mStartingChild != null) {
305             reset(mStartingChild!!)
306             mStartingChild = null
307         } else {
308             resetClock()
309         }
310         wakeUpCoordinator.setNotificationsVisibleForExpansion(false /* visible */,
311                 true /* animate */,
312                 false /* increaseSpeed */)
313     }
314 
315     private fun findView(x: Float, y: Float): ExpandableView? {
316         var totalX = x
317         var totalY = y
318         stackScroller.getLocationOnScreen(mTemp2)
319         totalX += mTemp2[0].toFloat()
320         totalY += mTemp2[1].toFloat()
321         val childAtRawPosition = stackScroller.getChildAtRawPosition(totalX, totalY)
322         return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) {
323             childAtRawPosition
324         } else null
325     }
326 
327     fun setUp(
328         stackScroller: NotificationStackScrollLayout,
329         expansionCallback: ExpansionCallback,
330         shadeController: ShadeController
331     ) {
332         this.expansionCallback = expansionCallback
333         this.shadeController = shadeController
334         this.stackScroller = stackScroller
335     }
336 
337     fun setPulsing(pulsing: Boolean) {
338         mPulsing = pulsing
339     }
340 
341     fun onStartedWakingUp() {
342         isWakingToShadeLocked = false
343     }
344 
345     interface ExpansionCallback {
346         fun setEmptyDragAmount(amount: Float)
347     }
348 }
349