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.InputDevice
22 import android.view.MotionEvent
23 import android.view.View
24 import android.view.ViewGroup
25 import android.view.ViewTreeObserver
26 import com.android.systemui.Gefingerpoken
27 import com.android.systemui.flags.FeatureFlags
28 import com.android.systemui.flags.Flags
29 import com.android.systemui.res.R
30 import com.android.systemui.scene.shared.flag.SceneContainerFlag
31 import com.android.systemui.scene.ui.view.WindowRootView
32 import com.android.systemui.shade.ShadeController
33 import com.android.systemui.shade.ShadeLogger
34 import com.android.systemui.shade.ShadeViewController
35 import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
36 import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator
37 import com.android.systemui.statusbar.policy.ConfigurationController
38 import com.android.systemui.statusbar.window.StatusBarWindowStateController
39 import com.android.systemui.unfold.SysUIUnfoldComponent
40 import com.android.systemui.unfold.UNFOLD_STATUS_BAR
41 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
42 import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel
43 import com.android.systemui.util.ViewController
44 import com.android.systemui.util.kotlin.getOrNull
45 import com.android.systemui.util.view.ViewUtil
46 import java.util.Optional
47 import javax.inject.Inject
48 import javax.inject.Named
49 import javax.inject.Provider
50 
51 private const val TAG = "PhoneStatusBarViewController"
52 
53 /** Controller for [PhoneStatusBarView]. */
54 class PhoneStatusBarViewController
55 private constructor(
56     view: PhoneStatusBarView,
57     @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?,
58     private val centralSurfaces: CentralSurfaces,
59     private val statusBarWindowStateController: StatusBarWindowStateController,
60     private val shadeController: ShadeController,
61     private val shadeViewController: ShadeViewController,
62     private val panelExpansionInteractor: PanelExpansionInteractor,
63     private val windowRootView: Provider<WindowRootView>,
64     private val shadeLogger: ShadeLogger,
65     private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?,
66     private val userChipViewModel: StatusBarUserChipViewModel,
67     private val viewUtil: ViewUtil,
68     private val configurationController: ConfigurationController,
69     private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
70 ) : ViewController<PhoneStatusBarView>(view) {
71 
72     private lateinit var statusContainer: View
73 
74     private val configurationListener =
75         object : ConfigurationController.ConfigurationListener {
76             override fun onDensityOrFontScaleChanged() {
77                 mView.onDensityOrFontScaleChanged()
78             }
79         }
80 
81     override fun onViewAttached() {
82         statusContainer = mView.requireViewById(R.id.system_icons)
83         statusContainer.setOnHoverListener(
84             statusOverlayHoverListenerFactory.createDarkAwareListener(statusContainer)
85         )
86         statusContainer.setOnTouchListener(object : View.OnTouchListener {
87             override fun onTouch(v: View, event: MotionEvent): Boolean {
88                 // We want to handle only mouse events here to avoid stealing finger touches from
89                 // status bar which expands shade when swiped down on. We're using onTouchListener
90                 // instead of onClickListener as the later will lead to isClickable being set to
91                 // true and hence ALL touches always being intercepted. See [View.OnTouchEvent]
92                 if (event.source == InputDevice.SOURCE_MOUSE) {
93                     if (event.action == MotionEvent.ACTION_UP) {
94                         v.performClick()
95                         shadeController.animateExpandShade()
96                     }
97                     return true
98                 }
99                 return false
100             }
101         })
102 
103         progressProvider?.setReadyToHandleTransition(true)
104         configurationController.addCallback(configurationListener)
105 
106         if (moveFromCenterAnimationController == null) return
107 
108         val statusBarLeftSide: View =
109             mView.requireViewById(R.id.status_bar_start_side_except_heads_up)
110         val systemIconArea: ViewGroup = mView.requireViewById(R.id.status_bar_end_side_content)
111 
112         val viewsToAnimate = arrayOf(statusBarLeftSide, systemIconArea)
113 
114         mView.viewTreeObserver.addOnPreDrawListener(
115             object : ViewTreeObserver.OnPreDrawListener {
116                 override fun onPreDraw(): Boolean {
117                     moveFromCenterAnimationController.onViewsReady(viewsToAnimate)
118                     mView.viewTreeObserver.removeOnPreDrawListener(this)
119                     return true
120                 }
121             }
122         )
123 
124         mView.addOnLayoutChangeListener { _, left, _, right, _, oldLeft, _, oldRight, _ ->
125             val widthChanged = right - left != oldRight - oldLeft
126             if (widthChanged) {
127                 moveFromCenterAnimationController.onStatusBarWidthChanged()
128             }
129         }
130     }
131 
132     override fun onViewDetached() {
133         statusContainer.setOnHoverListener(null)
134         progressProvider?.setReadyToHandleTransition(false)
135         moveFromCenterAnimationController?.onViewDetached()
136         configurationController.removeCallback(configurationListener)
137     }
138 
139     init {
140         mView.setTouchEventHandler(PhoneStatusBarViewTouchHandler())
141         mView.init(userChipViewModel)
142     }
143 
144     override fun onInit() {}
145 
146     fun setImportantForAccessibility(mode: Int) {
147         mView.importantForAccessibility = mode
148     }
149 
150     /**
151      * Sends a touch event to the status bar view.
152      *
153      * This is required in certain cases because the status bar view is in a separate window from
154      * the rest of SystemUI, and other windows may decide that their touch should instead be treated
155      * as a status bar window touch.
156      */
157     fun sendTouchToView(ev: MotionEvent): Boolean {
158         return mView.dispatchTouchEvent(ev)
159     }
160 
161     /**
162      * Returns true if the given (x, y) point (in screen coordinates) is within the status bar
163      * view's range and false otherwise.
164      */
165     fun touchIsWithinView(x: Float, y: Float): Boolean {
166         return viewUtil.touchIsWithinView(mView, x, y)
167     }
168 
169     /** Called when a touch event occurred on {@link PhoneStatusBarView}. */
170     fun onTouch(event: MotionEvent) {
171         if (statusBarWindowStateController.windowIsShowing()) {
172             val upOrCancel =
173                 event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL
174             centralSurfaces.setInteracting(
175                 WINDOW_STATUS_BAR,
176                 !upOrCancel || shadeController.isExpandedVisible
177             )
178         }
179     }
180 
181     inner class PhoneStatusBarViewTouchHandler : Gefingerpoken {
182         override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
183             onTouch(event)
184             return false
185         }
186 
187         override fun onTouchEvent(event: MotionEvent): Boolean {
188             onTouch(event)
189 
190             // If panels aren't enabled, ignore the gesture and don't pass it down to the
191             // panel view.
192             if (!centralSurfaces.commandQueuePanelsEnabled) {
193                 if (event.action == MotionEvent.ACTION_DOWN) {
194                     Log.v(
195                         TAG,
196                         String.format(
197                             "onTouchForwardedFromStatusBar: panel disabled, " +
198                                 "ignoring touch at (${event.x.toInt()},${event.y.toInt()})"
199                         )
200                     )
201                 }
202                 return false
203             }
204 
205             // If scene framework is enabled, route the touch to it and
206             // ignore the rest of the gesture.
207             if (SceneContainerFlag.isEnabled) {
208                 windowRootView.get().dispatchTouchEvent(event)
209                 return true
210             }
211 
212             if (event.action == MotionEvent.ACTION_DOWN) {
213                 // If the view that would receive the touch is disabled, just have status
214                 // bar eat the gesture.
215                 if (!shadeViewController.isViewEnabled) {
216                     shadeLogger.logMotionEvent(
217                         event,
218                         "onTouchForwardedFromStatusBar: panel view disabled"
219                     )
220                     return true
221                 }
222                 if (panelExpansionInteractor.isFullyCollapsed && event.y < 1f) {
223                     // b/235889526 Eat events on the top edge of the phone when collapsed
224                     shadeLogger.logMotionEvent(event, "top edge touch ignored")
225                     return true
226                 }
227             }
228             return shadeViewController.handleExternalTouch(event)
229         }
230     }
231 
232     class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider {
233         override fun getViewCenter(view: View, outPoint: Point) =
234             when (view.id) {
235                 R.id.status_bar_start_side_except_heads_up -> {
236                     // items aligned to the start, return start center point
237                     getViewEdgeCenter(view, outPoint, isStart = true)
238                 }
239                 R.id.status_bar_end_side_content -> {
240                     // items aligned to the end, return end center point
241                     getViewEdgeCenter(view, outPoint, isStart = false)
242                 }
243                 else -> super.getViewCenter(view, outPoint)
244             }
245 
246         /** Returns start or end (based on [isStart]) center point of the view */
247         private fun getViewEdgeCenter(view: View, outPoint: Point, isStart: Boolean) {
248             val isRtl = view.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
249             val isLeftEdge = isRtl xor isStart
250 
251             val viewLocation = IntArray(2)
252             view.getLocationOnScreen(viewLocation)
253 
254             val viewX = viewLocation[0]
255             val viewY = viewLocation[1]
256 
257             outPoint.x = viewX + if (isLeftEdge) view.height / 2 else view.width - view.height / 2
258             outPoint.y = viewY + view.height / 2
259         }
260     }
261 
262     class Factory
263     @Inject
264     constructor(
265         private val unfoldComponent: Optional<SysUIUnfoldComponent>,
266         @Named(UNFOLD_STATUS_BAR)
267         private val progressProvider: Optional<ScopedUnfoldTransitionProgressProvider>,
268         private val featureFlags: FeatureFlags,
269         private val userChipViewModel: StatusBarUserChipViewModel,
270         private val centralSurfaces: CentralSurfaces,
271         private val statusBarWindowStateController: StatusBarWindowStateController,
272         private val shadeController: ShadeController,
273         private val shadeViewController: ShadeViewController,
274         private val panelExpansionInteractor: PanelExpansionInteractor,
275         private val windowRootView: Provider<WindowRootView>,
276         private val shadeLogger: ShadeLogger,
277         private val viewUtil: ViewUtil,
278         private val configurationController: ConfigurationController,
279         private val statusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory,
280     ) {
281         fun create(view: PhoneStatusBarView): PhoneStatusBarViewController {
282             val statusBarMoveFromCenterAnimationController =
283                 if (featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
284                     unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
285                 } else {
286                     null
287                 }
288 
289             return PhoneStatusBarViewController(
290                 view,
291                 progressProvider.getOrNull(),
292                 centralSurfaces,
293                 statusBarWindowStateController,
294                 shadeController,
295                 shadeViewController,
296                 panelExpansionInteractor,
297                 windowRootView,
298                 shadeLogger,
299                 statusBarMoveFromCenterAnimationController,
300                 userChipViewModel,
301                 viewUtil,
302                 configurationController,
303                 statusOverlayHoverListenerFactory,
304             )
305         }
306     }
307 }
308