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