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