• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2021 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 package com.android.systemui.statusbar.phone
17 
18 import android.app.StatusBarManager.WINDOW_STATUS_BAR
19 import android.graphics.Point
20 import android.util.Log
21 import android.view.Display.DEFAULT_DISPLAY
22 import android.view.InputDevice
23 import android.view.MotionEvent
24 import android.view.View
25 import android.view.ViewGroup
26 import android.view.ViewTreeObserver
27 import androidx.annotation.VisibleForTesting
28 import com.android.systemui.Flags
29 import com.android.systemui.Gefingerpoken
30 import com.android.systemui.battery.BatteryMeterView
31 import com.android.systemui.dagger.qualifiers.DisplaySpecific
32 import com.android.systemui.flags.FeatureFlags
33 import com.android.systemui.flags.Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS
34 import com.android.systemui.plugins.DarkIconDispatcher
35 import com.android.systemui.res.R
36 import com.android.systemui.scene.shared.flag.SceneContainerFlag
37 import com.android.systemui.scene.ui.view.WindowRootView
38 import com.android.systemui.shade.ShadeController
39 import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress
40 import com.android.systemui.shade.ShadeLogger
41 import com.android.systemui.shade.ShadeViewController
42 import com.android.systemui.shade.StatusBarLongPressGestureDetector
43 import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy
44 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
45 import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround
46 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
47 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
48 import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore
49 import com.android.systemui.statusbar.policy.Clock
50 import com.android.systemui.statusbar.policy.ConfigurationController
51 import com.android.systemui.statusbar.window.StatusBarWindowStateController
52 import com.android.systemui.unfold.SysUIUnfoldComponent
53 import com.android.systemui.unfold.UNFOLD_STATUS_BAR
54 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
55 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
56 import com.android.systemui.util.ViewController
57 import com.android.systemui.util.kotlin.getOrNull
58 import com.android.systemui.util.view.ViewUtil
59 import dagger.Lazy
60 import java.util.Optional
61 import javax.inject.Inject
62 import javax.inject.Named
63 import javax.inject.Provider
64 
65 private const val TAG = "PhoneStatusBarViewController"
66 
67 /** Controller for [PhoneStatusBarView]. */
68 class PhoneStatusBarViewController
69 private constructor(
70     view: PhoneStatusBarView,
71     @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?,
72     private val centralSurfaces: CentralSurfaces,
73     private val statusBarWindowStateController: StatusBarWindowStateController,
74     private val shadeController: ShadeController,
75     private val shadeViewController: ShadeViewController,
76     private val panelExpansionInteractor: PanelExpansionInteractor,
77     private val statusBarLongPressGestureDetector: Provider<StatusBarLongPressGestureDetector>,
78     private val windowRootView: Provider<WindowRootView>,
79     private val shadeLogger: ShadeLogger,
80     private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
81     private val userChipViewModel: StatusBarUserChipViewModel,
82     private val viewUtil: ViewUtil,
83     private val configurationController: ConfigurationController,
84     private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
85     private val darkIconDispatcher: DarkIconDispatcher,
86     private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
87     private val lazyStatusBarShadeDisplayPolicy: Lazy<StatusBarTouchShadeDisplayPolicy>,
88 ) : ViewController<PhoneStatusBarView>(view) {
89 
90     private lateinit var battery: BatteryMeterView
91     private lateinit var clock: Clock
92     private lateinit var startSideContainer: View
93     private lateinit var endSideContainer: View
94     private val statusBarContentInsetsProvider
95         get() = statusBarContentInsetsProviderStore.forDisplay(context.displayId)
96 
97     private val iconsOnTouchListener =
98         object : View.OnTouchListener {
99             override fun onTouch(v: View, event: MotionEvent): Boolean {
100                 // We want to handle only mouse events here to avoid stealing finger touches
101                 // from status bar which expands shade when swiped down on. See b/326097469.
102                 // We're using onTouchListener instead of onClickListener as the later will lead
103                 // to isClickable being set to true and hence ALL touches always being
104                 // intercepted. See [View.OnTouchEvent]
105                 if (event.source == InputDevice.SOURCE_MOUSE) {
106                     if (event.action == MotionEvent.ACTION_UP) {
107                         dispatchEventToShadeDisplayPolicy(event)
108                         v.performClick()
109                         shadeController.animateExpandShade()
110                     }
111                     return true
112                 }
113                 return false
114             }
115         }
116 
117     private fun dispatchEventToShadeDisplayPolicy(event: MotionEvent) {
118         if (ShadeWindowGoesAround.isEnabled) {
119             // Notify the shade display policy that the status bar was touched. This may cause
120             // the shade to change display if the touch was in a display different than the shade
121             // one.
122             lazyStatusBarShadeDisplayPolicy.get().onStatusBarTouched(event, mView.width)
123         }
124     }
125 
126     private val configurationListener =
127         object : ConfigurationController.ConfigurationListener {
128             override fun onDensityOrFontScaleChanged() {
129                 clock.onDensityOrFontScaleChanged()
130             }
131         }
132 
133     override fun onViewAttached() {
134         clock = mView.requireViewById(R.id.clock)
135         battery = mView.requireViewById(R.id.battery)
136         addDarkReceivers()
137         addCursorSupportToIconContainers()
138 
139         if (ShadeExpandsOnStatusBarLongPress.isEnabled) {
140             mView.setLongPressGestureDetector(statusBarLongPressGestureDetector.get())
141         }
142 
143         progressProvider?.setReadyToHandleTransition(true)
144         configurationController.addCallback(configurationListener)
145 
146         if (moveFromCenterAnimationController == null) return
147 
148         val statusBarLeftSide: View =
149             mView.requireViewById(R.id.status_bar_start_side_except_heads_up)
150         val systemIconArea: ViewGroup = mView.requireViewById(R.id.status_bar_end_side_content)
151 
152         val viewsToAnimate = arrayOf(statusBarLeftSide, systemIconArea)
153 
154         mView.viewTreeObserver.addOnPreDrawListener(
155             object : ViewTreeObserver.OnPreDrawListener {
156                 override fun onPreDraw(): Boolean {
157                     moveFromCenterAnimationController.onViewsReady(viewsToAnimate)
158                     mView.viewTreeObserver.removeOnPreDrawListener(this)
159                     return true
160                 }
161             }
162         )
163 
164         mView.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ ->
165             val widthChanged = right - left != oldRight - oldLeft
166             if (widthChanged) {
167                 moveFromCenterAnimationController.onStatusBarWidthChanged()
168             }
169         }
170     }
171 
172     private fun addCursorSupportToIconContainers() {
173         endSideContainer = mView.requireViewById(R.id.system_icons)
174         endSideContainer.setOnHoverListener(
175             statusOverlayHoverListenerFactory.createDarkAwareListener(endSideContainer)
176         )
177         endSideContainer.setOnTouchListener(iconsOnTouchListener)
178 
179         startSideContainer = mView.requireViewById(R.id.status_bar_start_side_content)
180         startSideContainer.setOnHoverListener(
181             statusOverlayHoverListenerFactory.createDarkAwareListener(
182                 startSideContainer,
183                 topHoverMargin = 6,
184                 bottomHoverMargin = 6,
185             )
186         )
187         startSideContainer.setOnTouchListener(iconsOnTouchListener)
188     }
189 
190     @VisibleForTesting
191     public override fun onViewDetached() {
192         removeDarkReceivers()
193         startSideContainer.setOnHoverListener(null)
194         endSideContainer.setOnHoverListener(null)
195         progressProvider?.setReadyToHandleTransition(false)
196         moveFromCenterAnimationController?.onViewDetached()
197         configurationController.removeCallback(configurationListener)
198     }
199 
200     init {
201         // These should likely be done in `onInit`, not `init`.
202         mView.setTouchEventHandler(PhoneStatusBarViewTouchHandler())
203         statusBarContentInsetsProvider?.let {
204             mView.setHasCornerCutoutFetcher { it.currentRotationHasCornerCutout() }
205             mView.setInsetsFetcher { it.getStatusBarContentInsetsForCurrentRotation() }
206         }
207         mView.init(userChipViewModel)
208     }
209 
210     override fun onInit() {}
211 
212     fun setImportantForAccessibility(mode: Int) {
213         mView.importantForAccessibility = mode
214     }
215 
216     /**
217      * Sends a touch event to the status bar view.
218      *
219      * This is required in certain cases because the status bar view is in a separate window from
220      * the rest of SystemUI, and other windows may decide that their touch should instead be treated
221      * as a status bar window touch.
222      */
223     fun sendTouchToView(ev: MotionEvent): Boolean {
224         return mView.dispatchTouchEvent(ev)
225     }
226 
227     /**
228      * Returns true if the given (x, y) point (in screen coordinates) is within the status bar
229      * view's range and false otherwise.
230      */
231     fun touchIsWithinView(x: Float, y: Float): Boolean {
232         return viewUtil.touchIsWithinView(mView, x, y)
233     }
234 
235     /** Called when a touch event occurred on {@link PhoneStatusBarView}. */
236     fun onTouch(event: MotionEvent) {
237         if (statusBarWindowStateController.windowIsShowing()) {
238             val upOrCancel =
239                 event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL
240             centralSurfaces.setInteracting(
241                 WINDOW_STATUS_BAR,
242                 !upOrCancel || shadeController.isExpandedVisible,
243             )
244         }
245     }
246 
247     private fun addDarkReceivers() {
248         darkIconDispatcher.addDarkReceiver(battery)
249         darkIconDispatcher.addDarkReceiver(clock)
250     }
251 
252     private fun removeDarkReceivers() {
253         darkIconDispatcher.removeDarkReceiver(battery)
254         darkIconDispatcher.removeDarkReceiver(clock)
255     }
256 
257     inner class PhoneStatusBarViewTouchHandler : Gefingerpoken {
258         override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
259             if (event.action == MotionEvent.ACTION_DOWN) {
260                 dispatchEventToShadeDisplayPolicy(event)
261             }
262             return if (Flags.statusBarSwipeOverChip()) {
263                 shadeViewController.handleExternalInterceptTouch(event)
264             } else {
265                 onTouch(event)
266                 false
267             }
268         }
269 
270         override fun onTouchEvent(event: MotionEvent): Boolean {
271             onTouch(event)
272 
273             // If panels aren't enabled, ignore the gesture and don't pass it down to the
274             // panel view.
275             if (!centralSurfaces.commandQueuePanelsEnabled) {
276                 if (event.action == MotionEvent.ACTION_DOWN) {
277                     Log.v(
278                         TAG,
279                         String.format(
280                             "onTouchForwardedFromStatusBar: panel disabled, " +
281                                 "ignoring touch at (${event.x.toInt()},${event.y.toInt()})"
282                         ),
283                     )
284                 }
285                 return false
286             }
287 
288             // If scene framework is enabled, route the touch to it and
289             // ignore the rest of the gesture.
290             if (SceneContainerFlag.isEnabled) {
291                 windowRootView.get().dispatchTouchEvent(event)
292                 return true
293             }
294 
295             if (event.action == MotionEvent.ACTION_DOWN) {
296                 // If the view that would receive the touch is disabled, just have status
297                 // bar eat the gesture.
298                 if (!shadeViewController.isViewEnabled) {
299                     shadeLogger.logMotionEvent(
300                         event,
301                         "onTouchForwardedFromStatusBar: panel view disabled",
302                     )
303                     return true
304                 }
305                 if (panelExpansionInteractor.isFullyCollapsed && event.y < 1f) {
306                     // b/235889526 Eat events on the top edge of the phone when collapsed
307                     shadeLogger.logMotionEvent(event, "top edge touch ignored")
308                     return true
309                 }
310             }
311 
312             // With the StatusBarConnectedDisplays changes, status bar touches should result in
313             // shade interaction only if ShadeWindowGoesAround.isEnabled or if touch is on default
314             // display.
315             return if (
316                 !StatusBarConnectedDisplays.isEnabled ||
317                     ShadeWindowGoesAround.isEnabled ||
318                     context.displayId == DEFAULT_DISPLAY
319             ) {
320                 shadeViewController.handleExternalTouch(event)
321             } else {
322                 false
323             }
324         }
325     }
326 
327     class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider {
328         override fun getViewCenter(view: View, outPoint: Point) =
329             when (view.id) {
330                 R.id.status_bar_start_side_except_heads_up -> {
331                     // items aligned to the start, return start center point
332                     getViewEdgeCenter(view, outPoint, isStart = true)
333                 }
334                 R.id.status_bar_end_side_content -> {
335                     // items aligned to the end, return end center point
336                     getViewEdgeCenter(view, outPoint, isStart = false)
337                 }
338                 else -> super.getViewCenter(view, outPoint)
339             }
340 
341         /** Returns start or end (based on [isStart]) center point of the view */
342         private fun getViewEdgeCenter(view: View, outPoint: Point, isStart: Boolean) {
343             val isRtl = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
344             val isLeftEdge = isRtl xor isStart
345 
346             val viewLocation = IntArray(2)
347             view.getLocationOnScreen(viewLocation)
348 
349             val viewX = viewLocation[0]
350             val viewY = viewLocation[1]
351 
352             outPoint.x = viewX + if (isLeftEdge) view.height / 2 else view.width - view.height / 2
353             outPoint.y = viewY + view.height / 2
354         }
355     }
356 
357     class Factory
358     @Inject
359     constructor(
360         private val unfoldComponent: Optional<SysUIUnfoldComponent>,
361         @Named(UNFOLD_STATUS_BAR)
362         private val progressProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
363         private val featureFlags: FeatureFlags,
364         private val userChipViewModel: StatusBarUserChipViewModel,
365         private val centralSurfaces: CentralSurfaces,
366         private val statusBarWindowStateController: StatusBarWindowStateController,
367         private val shadeController: ShadeController,
368         private val shadeViewController: ShadeViewController,
369         private val panelExpansionInteractor: PanelExpansionInteractor,
370         private val statusBarLongPressGestureDetector: Provider<StatusBarLongPressGestureDetector>,
371         private val windowRootView: Provider<WindowRootView>,
372         private val shadeLogger: ShadeLogger,
373         private val viewUtil: ViewUtil,
374         private val configurationController: ConfigurationController,
375         private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
376         @DisplaySpecific private val darkIconDispatcher: DarkIconDispatcher,
377         private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
378         private val lazyStatusBarShadeDisplayPolicy: Lazy<StatusBarTouchShadeDisplayPolicy>,
379     ) {
380         fun create(view: PhoneStatusBarView): PhoneStatusBarViewController {
381             val statusBarMoveFromCenterAnimationController =
382                 if (featureFlags.isEnabled(ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
383                     unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
384                 } else {
385                     null
386                 }
387 
388             return PhoneStatusBarViewController(
389                 view,
390                 progressProvider.getOrNull(),
391                 centralSurfaces,
392                 statusBarWindowStateController,
393                 shadeController,
394                 shadeViewController,
395                 panelExpansionInteractor,
396                 statusBarLongPressGestureDetector,
397                 windowRootView,
398                 shadeLogger,
399                 statusBarMoveFromCenterAnimationController,
400                 userChipViewModel,
401                 viewUtil,
402                 configurationController,
403                 statusOverlayHoverListenerFactory,
404                 darkIconDispatcher,
405                 statusBarContentInsetsProviderStore,
406                 lazyStatusBarShadeDisplayPolicy,
407             )
408         }
409     }
410 }
411