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.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.app.animation.Interpolators
32 import com.android.systemui.Dumpable
33 import com.android.systemui.Gefingerpoken
34 import com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN
35 import com.android.systemui.dagger.SysUISingleton
36 import com.android.systemui.dump.DumpManager
37 import com.android.systemui.plugins.FalsingManager
38 import com.android.systemui.plugins.statusbar.StatusBarStateController
39 import com.android.systemui.res.R
40 import com.android.systemui.shade.domain.interactor.ShadeInteractor
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.NotificationStackScrollLayoutController
45 import com.android.systemui.statusbar.phone.KeyguardBypassController
46 import com.android.systemui.statusbar.policy.ConfigurationController
47 import com.android.systemui.statusbar.policy.HeadsUpManager
48 import java.io.PrintWriter
49 import javax.inject.Inject
50 import kotlin.math.max
51 
52 /**
53  * A utility class that handles notification panel expansion when a user swipes downward on a
54  * notification from the pulsing state.
55  * If face-bypass is enabled, the user can swipe down anywhere on the screen (not just from a
56  * notification) to trigger the notification panel expansion.
57  */
58 @SysUISingleton
59 class PulseExpansionHandler @Inject
60 constructor(
61     context: Context,
62     private val wakeUpCoordinator: NotificationWakeUpCoordinator,
63     private val bypassController: KeyguardBypassController,
64     private val headsUpManager: HeadsUpManager,
65     configurationController: ConfigurationController,
66     private val statusBarStateController: StatusBarStateController,
67     private val falsingManager: FalsingManager,
68     private val shadeInteractor: ShadeInteractor,
69     private val lockscreenShadeTransitionController: LockscreenShadeTransitionController,
70     dumpManager: DumpManager
71 ) : Gefingerpoken, Dumpable {
72     companion object {
73         private val SPRING_BACK_ANIMATION_LENGTH_MS = 375
74     }
75 
76     private val mPowerManager: PowerManager?
77 
78     private var mInitialTouchX: Float = 0.0f
79     private var mInitialTouchY: Float = 0.0f
80     var isExpanding: Boolean = false
81         private set(value) {
82             val changed = field != value
83             field = value
84             bypassController.isPulseExpanding = value
85             if (changed) {
86                 if (value) {
87                     lockscreenShadeTransitionController.onPulseExpansionStarted()
88                 } else {
89                     if (!leavingLockscreen) {
90                         bypassController.maybePerformPendingUnlock()
91                         pulseExpandAbortListener?.run()
92                     }
93                 }
94                 headsUpManager.unpinAll(
95                     /*userUnPinned= */
96                     true,
97                 )
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 pulseExpandAbortListener: Runnable? = null
115     var bouncerShowing: Boolean = false
116 
117     init {
118         initResources(context)
119         configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
120             override fun onConfigChanged(newConfig: Configuration?) {
121                 initResources(context)
122             }
123         })
124 
125         mPowerManager = context.getSystemService(PowerManager::class.java)
126         dumpManager.registerDumpable(this)
127     }
128 
129     private fun initResources(context: Context) {
130         minDragDistance = context.resources.getDimensionPixelSize(
131             R.dimen.keyguard_drag_down_min_distance)
132         touchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
133     }
134 
135     override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
136         return canHandleMotionEvent() && startExpansion(event)
137     }
138 
139     private fun canHandleMotionEvent(): Boolean {
140         return wakeUpCoordinator.canShowPulsingHuns && !shadeInteractor.isQsExpanded.value &&
141             !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                     isExpanding = true
166                     captureStartingChild(mInitialTouchX, mInitialTouchY)
167                     mInitialTouchY = y
168                     mInitialTouchX = x
169                     return true
170                 }
171             }
172 
173             MotionEvent.ACTION_UP -> {
174                 recycleVelocityTracker()
175                 isExpanding = false
176             }
177 
178             MotionEvent.ACTION_CANCEL -> {
179                 recycleVelocityTracker()
180                 isExpanding = false
181             }
182         }
183         return false
184     }
185 
186     private fun recycleVelocityTracker() {
187         velocityTracker?.recycle()
188         velocityTracker = null
189     }
190 
191     override fun onTouchEvent(event: MotionEvent): Boolean {
192         val finishExpanding = (event.action == MotionEvent.ACTION_CANCEL ||
193             event.action == MotionEvent.ACTION_UP) && isExpanding
194 
195         val isDraggingNotificationOrCanBypass = mStartingChild?.showingPulsing() == true ||
196             bypassController.canBypass()
197         if ((!canHandleMotionEvent() || !isDraggingNotificationOrCanBypass) && !finishExpanding) {
198             // We allow cancellations/finishing to still go through here to clean up the state
199             return false
200         }
201 
202         if (velocityTracker == null || !isExpanding ||
203             event.actionMasked == MotionEvent.ACTION_DOWN) {
204             return startExpansion(event)
205         }
206         velocityTracker!!.addMovement(event)
207         val y = event.y
208 
209         val moveDistance = y - mInitialTouchY
210         when (event.actionMasked) {
211             MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance)
212             MotionEvent.ACTION_UP -> {
213                 velocityTracker!!.computeCurrentVelocity(
214                     /* units= */
215                     1000,
216                 )
217                 val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000 &&
218                     statusBarStateController.state != StatusBarState.SHADE
219                 if (!falsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) {
220                     finishExpansion()
221                 } else {
222                     cancelExpansion()
223                 }
224                 recycleVelocityTracker()
225             }
226 
227             MotionEvent.ACTION_CANCEL -> {
228                 cancelExpansion()
229                 recycleVelocityTracker()
230             }
231         }
232         return isExpanding
233     }
234 
235     private fun finishExpansion() {
236         val startingChild = mStartingChild
237         if (mStartingChild != null) {
238             setUserLocked(mStartingChild!!, false)
239             mStartingChild = null
240         }
241         if (statusBarStateController.isDozing) {
242             wakeUpCoordinator.willWakeUp = true
243             mPowerManager!!.wakeUp(
244                 SystemClock.uptimeMillis(),
245                 PowerManager.WAKE_REASON_GESTURE,
246                 "com.android.systemui:PULSEDRAG"
247             )
248         }
249         lockscreenShadeTransitionController.goToLockedShade(
250             startingChild,
251             needsQSAnimation = false
252         )
253         lockscreenShadeTransitionController.finishPulseAnimation(cancelled = false)
254         leavingLockscreen = true
255         isExpanding = false
256         if (mStartingChild is ExpandableNotificationRow) {
257             val row = mStartingChild as ExpandableNotificationRow?
258             row!!.onExpandedByGesture(
259                 /*userExpanded= */
260                 true,
261             )
262         }
263     }
264 
265     private fun updateExpansionHeight(height: Float) {
266         var expansionHeight = max(height, 0.0f)
267         if (mStartingChild != null) {
268             val child = mStartingChild!!
269             val newHeight = Math.min(
270                 (child.collapsedHeight + expansionHeight).toInt(),
271                 child.maxContentHeight
272             )
273             child.actualHeight = newHeight
274         } else {
275             wakeUpCoordinator.setNotificationsVisibleForExpansion(
276                 height
277                     > lockscreenShadeTransitionController.distanceUntilShowingPulsingNotifications,
278                 /*animate= */
279                 true,
280                 /*increaseSpeed= */
281                 true
282             )
283         }
284         lockscreenShadeTransitionController.setPulseHeight(expansionHeight, animate = false)
285     }
286 
287     private fun captureStartingChild(x: Float, y: Float) {
288         if (mStartingChild == null && !bypassController.bypassEnabled) {
289             mStartingChild = findView(x, y)
290             if (mStartingChild != null) {
291                 setUserLocked(mStartingChild!!, true)
292             }
293         }
294     }
295 
296     @VisibleForTesting
297     fun reset(
298         child: ExpandableView,
299         animationDuration: Long = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
300     ) {
301         if (child.actualHeight == child.collapsedHeight) {
302             setUserLocked(child, false)
303             return
304         }
305         val anim = ValueAnimator.ofInt(child.actualHeight, child.collapsedHeight)
306         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
307         anim.duration = animationDuration
308         anim.addUpdateListener { animation: ValueAnimator ->
309             // don't use reflection, because the `actualHeight` field may be obfuscated
310             child.actualHeight = animation.animatedValue as Int
311         }
312         anim.addListener(object : AnimatorListenerAdapter() {
313             override fun onAnimationEnd(animation: Animator) {
314                 setUserLocked(child, false)
315             }
316         })
317         anim.start()
318     }
319 
320     private fun setUserLocked(child: ExpandableView, userLocked: Boolean) {
321         if (child is ExpandableNotificationRow) {
322             child.isUserLocked = userLocked
323         }
324     }
325 
326     private fun cancelExpansion() {
327         isExpanding = false
328         if (mStartingChild != null) {
329             reset(mStartingChild!!)
330             mStartingChild = null
331         }
332         lockscreenShadeTransitionController.finishPulseAnimation(cancelled = true)
333         wakeUpCoordinator.setNotificationsVisibleForExpansion(
334             /*visible= */
335             false,
336             /*animate= */
337             true,
338             /*increaseSpeed= */
339             false
340         )
341     }
342 
343     private fun findView(x: Float, y: Float): ExpandableView? {
344         var totalX = x
345         var totalY = y
346         stackScrollerController.getLocationOnScreen(mTemp2)
347         totalX += mTemp2[0].toFloat()
348         totalY += mTemp2[1].toFloat()
349         val childAtRawPosition = stackScrollerController.getChildAtRawPosition(totalX, totalY)
350         return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) {
351             childAtRawPosition
352         } else {
353             null
354         }
355     }
356 
357     fun setUp(stackScrollerController: NotificationStackScrollLayoutController) {
358         this.stackScrollerController = stackScrollerController
359     }
360 
361     fun setPulsing(pulsing: Boolean) {
362         mPulsing = pulsing
363     }
364 
365     override fun dump(pw: PrintWriter, args: Array<out String>) {
366         IndentingPrintWriter(pw, "  ").let {
367             it.println("PulseExpansionHandler:")
368             it.increaseIndent()
369             it.println("isExpanding: $isExpanding")
370             it.println("leavingLockscreen: $leavingLockscreen")
371             it.println("mPulsing: $mPulsing")
372             it.println("bouncerShowing: $bouncerShowing")
373         }
374     }
375 }
376