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