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