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