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