1 /* <lambda>null2 * Copyright (C) 2022 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.launcher3.taskbar 17 18 import android.graphics.Canvas 19 import android.graphics.Color 20 import android.graphics.Insets 21 import android.graphics.Paint 22 import android.graphics.Rect 23 import android.graphics.Region 24 import android.os.Binder 25 import android.os.IBinder 26 import android.view.DisplayInfo 27 import android.view.Gravity 28 import android.view.InsetsFrameProvider 29 import android.view.InsetsFrameProvider.SOURCE_DISPLAY 30 import android.view.InsetsSource.FLAG_ANIMATE_RESIZING 31 import android.view.InsetsSource.FLAG_INSETS_ROUNDED_CORNER 32 import android.view.InsetsSource.FLAG_SUPPRESS_SCRIM 33 import android.view.Surface 34 import android.view.ViewTreeObserver 35 import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME 36 import android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION 37 import android.view.WindowInsets 38 import android.view.WindowInsets.Type.mandatorySystemGestures 39 import android.view.WindowInsets.Type.navigationBars 40 import android.view.WindowInsets.Type.systemGestures 41 import android.view.WindowInsets.Type.tappableElement 42 import android.view.WindowManager 43 import android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD 44 import android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION 45 import androidx.core.graphics.toRegion 46 import com.android.internal.policy.GestureNavigationSettingsObserver 47 import com.android.launcher3.DeviceProfile 48 import com.android.launcher3.R 49 import com.android.launcher3.anim.AlphaUpdateListener 50 import com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION 51 import com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate 52 import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController 53 import com.android.launcher3.testing.shared.ResourceUtils 54 import com.android.launcher3.util.Executors 55 import java.io.PrintWriter 56 import kotlin.jvm.optionals.getOrNull 57 58 /** Handles the insets that Taskbar provides to underlying apps and the IME. */ 59 class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTaskbarController { 60 61 companion object { 62 private const val INDEX_LEFT = 0 63 private const val INDEX_RIGHT = 1 64 65 private fun Region.addBoundsToRegion(bounds: Rect?) { 66 bounds?.let { op(it, Region.Op.UNION) } 67 } 68 } 69 70 /** The bottom insets taskbar provides to the IME when IME is visible. */ 71 val taskbarHeightForIme: Int = context.resources.getDimensionPixelSize(R.dimen.taskbar_ime_size) 72 // The touchableRegion we will set unless some other state takes precedence. 73 private val defaultTouchableRegion: Region = Region() 74 private val insetsOwner: IBinder = Binder() 75 private val deviceProfileChangeListener = { _: DeviceProfile -> 76 onTaskbarOrBubblebarWindowHeightOrInsetsChanged() 77 } 78 private val gestureNavSettingsObserver = 79 GestureNavigationSettingsObserver( 80 context.mainThreadHandler, 81 Executors.UI_HELPER_EXECUTOR.handler, 82 context, 83 this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged, 84 ) 85 private val debugTouchableRegion = DebugTouchableRegion() 86 87 // Initialized in init. 88 private lateinit var controllers: TaskbarControllers 89 private lateinit var windowLayoutParams: WindowManager.LayoutParams 90 91 fun init(controllers: TaskbarControllers) { 92 this.controllers = controllers 93 windowLayoutParams = context.windowLayoutParams 94 onTaskbarOrBubblebarWindowHeightOrInsetsChanged() 95 96 context.addOnDeviceProfileChangeListener(deviceProfileChangeListener) 97 gestureNavSettingsObserver.registerForCallingUser() 98 } 99 100 fun onDestroy() { 101 context.removeOnDeviceProfileChangeListener(deviceProfileChangeListener) 102 gestureNavSettingsObserver.unregister() 103 } 104 105 fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() { 106 val taskbarStashController = controllers.taskbarStashController 107 val tappableHeight = taskbarStashController.tappableHeightToReportToApps 108 // We only report tappableElement height for unstashed, persistent taskbar, 109 // which is also when we draw the rounded corners above taskbar. 110 val insetsRoundedCornerFlag = 111 if (tappableHeight > 0) { 112 FLAG_INSETS_ROUNDED_CORNER 113 } else { 114 0 115 } 116 117 windowLayoutParams.providedInsets = 118 if (enableTaskbarNoRecreate() && controllers.sharedState != null) { 119 getProvidedInsets( 120 controllers.sharedState!!.insetsFrameProviders, 121 insetsRoundedCornerFlag, 122 ) 123 } else { 124 getProvidedInsets(insetsRoundedCornerFlag) 125 } 126 127 if (windowLayoutParams.paramsForRotation != null) { 128 for (layoutParams in windowLayoutParams.paramsForRotation) { 129 layoutParams.providedInsets = getProvidedInsets(insetsRoundedCornerFlag) 130 } 131 } 132 133 val bubbleControllers = controllers.bubbleControllers.getOrNull() 134 val taskbarTouchableHeight = taskbarStashController.touchableHeight 135 val bubblesTouchableHeight = 136 bubbleControllers?.bubbleStashController?.getTouchableHeight() ?: 0 137 // reset touch bounds 138 defaultTouchableRegion.setEmpty() 139 if (bubbleControllers != null) { 140 val bubbleBarViewController = bubbleControllers.bubbleBarViewController 141 val isBubbleBarVisible = bubbleControllers.bubbleStashController.isBubbleBarVisible() 142 val isAnimatingNewBubble = bubbleBarViewController.isAnimatingNewBubble 143 // if bubble bar is visible or animating new bubble, add bar bounds to the touch region 144 if (isBubbleBarVisible || isAnimatingNewBubble) { 145 defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.bubbleBarBounds) 146 defaultTouchableRegion.addBoundsToRegion(bubbleBarViewController.flyoutBounds) 147 } 148 } 149 if ( 150 taskbarStashController.isInApp || 151 controllers.uiController.isInOverviewUi || 152 context.showLockedTaskbarOnHome() 153 ) { 154 // only add the taskbar touch region if not on home 155 val bottom = windowLayoutParams.height 156 val top = bottom - taskbarTouchableHeight 157 val right = context.deviceProfile.widthPx 158 defaultTouchableRegion.addBoundsToRegion(Rect(/* left= */ 0, top, right, bottom)) 159 } 160 161 // Pre-calculate insets for different providers across different rotations for this gravity 162 for (rotation in Surface.ROTATION_0..Surface.ROTATION_270) { 163 // Add insets for navbar rotated params 164 val layoutParams = windowLayoutParams.paramsForRotation[rotation] 165 for (provider in layoutParams.providedInsets) { 166 setProviderInsets(provider, layoutParams.gravity, rotation) 167 } 168 } 169 // Also set the parent providers (i.e. not in paramsForRotation). 170 for (provider in windowLayoutParams.providedInsets) { 171 setProviderInsets(provider, windowLayoutParams.gravity, context.display.rotation) 172 } 173 context.notifyUpdateLayoutParams() 174 } 175 176 /** 177 * This is for when ENABLE_TASKBAR_NO_RECREATION is enabled. We generate one instance of 178 * providedInsets and use it across the entire lifecycle of TaskbarManager. The only thing we 179 * need to reset is nav bar flags based on insetsRoundedCornerFlag. 180 */ 181 private fun getProvidedInsets( 182 providedInsets: Array<InsetsFrameProvider>, 183 insetsRoundedCornerFlag: Int, 184 ): Array<InsetsFrameProvider> { 185 val navBarsFlag = 186 (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM else 0) or insetsRoundedCornerFlag 187 for (provider in providedInsets) { 188 if (provider.type == navigationBars()) { 189 provider.setFlags(navBarsFlag, FLAG_SUPPRESS_SCRIM or FLAG_INSETS_ROUNDED_CORNER) 190 } 191 } 192 return providedInsets 193 } 194 195 /** 196 * The inset types and number of insets provided have to match for both gesture nav and button 197 * nav. The values and the order of the elements in array are allowed to differ. Reason being WM 198 * does not allow types and number of insets changing for a given window once it is added into 199 * the hierarchy for performance reasons. 200 */ 201 private fun getProvidedInsets(insetsRoundedCornerFlag: Int): Array<InsetsFrameProvider> { 202 val navBarsFlag = 203 (if (context.isGestureNav) FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING else 0) or 204 insetsRoundedCornerFlag 205 return arrayOf( 206 InsetsFrameProvider(insetsOwner, 0, navigationBars()) 207 .setFlags( 208 navBarsFlag, 209 FLAG_SUPPRESS_SCRIM or FLAG_ANIMATE_RESIZING or FLAG_INSETS_ROUNDED_CORNER, 210 ), 211 InsetsFrameProvider(insetsOwner, 0, tappableElement()), 212 InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()), 213 InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures()) 214 .setSource(SOURCE_DISPLAY), 215 InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures()) 216 .setSource(SOURCE_DISPLAY), 217 ) 218 } 219 220 private fun setProviderInsets(provider: InsetsFrameProvider, gravity: Int, endRotation: Int) { 221 val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps 222 val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps 223 val res = context.resources 224 if (provider.type == navigationBars()) { 225 provider.insetsSize = getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) 226 } else if (provider.type == mandatorySystemGestures()) { 227 if (context.isThreeButtonNav) { 228 provider.insetsSize = 229 getInsetsForGravityWithCutout(contentHeight, gravity, endRotation) 230 } else { 231 val gestureHeight = 232 ResourceUtils.getNavbarSize( 233 ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, 234 context.resources, 235 ) 236 val isPinnedTaskbar = 237 context.deviceProfile.isTaskbarPresent && !context.isTransientTaskbar 238 val mandatoryGestureHeight = if (isPinnedTaskbar) contentHeight else gestureHeight 239 provider.insetsSize = 240 getInsetsForGravityWithCutout(mandatoryGestureHeight, gravity, endRotation) 241 } 242 } else if (provider.type == tappableElement()) { 243 provider.insetsSize = getInsetsForGravity(tappableHeight, gravity) 244 } else if (provider.type == systemGestures() && provider.index == INDEX_LEFT) { 245 val leftIndexInset = 246 if (context.isThreeButtonNav) 0 247 else gestureNavSettingsObserver.getLeftSensitivityForCallingUser(res) 248 provider.insetsSize = Insets.of(leftIndexInset, 0, 0, 0) 249 } else if (provider.type == systemGestures() && provider.index == INDEX_RIGHT) { 250 val rightIndexInset = 251 if (context.isThreeButtonNav) 0 252 else gestureNavSettingsObserver.getRightSensitivityForCallingUser(res) 253 provider.insetsSize = Insets.of(0, 0, rightIndexInset, 0) 254 } 255 256 // When in gesture nav, report the stashed height to the IME, to allow hiding the 257 // IME navigation bar. 258 val imeInsetsSize = 259 if (context.isGestureNav) { 260 getInsetsForGravity(controllers.taskbarStashController.stashedHeight, gravity) 261 } else { 262 getInsetsForGravity(taskbarHeightForIme, gravity) 263 } 264 val imeInsetsSizeOverride = 265 arrayOf( 266 InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize), 267 InsetsFrameProvider.InsetsSizeOverride( 268 TYPE_VOICE_INTERACTION, 269 // No-op override to keep the size and types in sync with the 270 // override below (insetsSizeOverrides must have the same length and 271 // types after the window is added according to 272 // WindowManagerService#relayoutWindow) 273 provider.insetsSize, 274 ), 275 ) 276 // Use 0 tappableElement insets for the VoiceInteractionWindow when gesture nav is enabled. 277 val visInsetsSizeForTappableElement = 278 if (context.isGestureNav) getInsetsForGravity(0, gravity) 279 else getInsetsForGravity(tappableHeight, gravity) 280 val insetsSizeOverrideForTappableElement = 281 arrayOf( 282 InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, imeInsetsSize), 283 InsetsFrameProvider.InsetsSizeOverride( 284 TYPE_VOICE_INTERACTION, 285 visInsetsSizeForTappableElement, 286 ), 287 ) 288 if ( 289 (context.isGestureNav || ENABLE_TASKBAR_NAVBAR_UNIFICATION) && 290 provider.type == tappableElement() 291 ) { 292 provider.insetsSizeOverrides = insetsSizeOverrideForTappableElement 293 } else if (provider.type != systemGestures()) { 294 // We only override insets at the bottom of the screen 295 provider.insetsSizeOverrides = imeInsetsSizeOverride 296 } 297 } 298 299 /** 300 * Calculate the [Insets] for taskbar after a rotation, specifically for any potential cutouts 301 * in the screen that can come from the camera. 302 */ 303 private fun getInsetsForGravityWithCutout(inset: Int, gravity: Int, rot: Int): Insets { 304 val display = context.display 305 // If there is no cutout, fall back to the original method of calculating insets 306 val cutout = display.cutout ?: return getInsetsForGravity(inset, gravity) 307 val rotation = display.rotation 308 val info = DisplayInfo() 309 display.getDisplayInfo(info) 310 val rotatedCutout = cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, rot) 311 312 if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) { 313 return Insets.of(0, 0, 0, maxOf(inset, rotatedCutout.safeInsetBottom)) 314 } 315 316 // TODO(b/230394142): seascape 317 val isSeascape = (gravity and Gravity.START) == Gravity.START 318 val leftInset = if (isSeascape) maxOf(inset, rotatedCutout.safeInsetLeft) else 0 319 val rightInset = if (isSeascape) 0 else maxOf(inset, rotatedCutout.safeInsetRight) 320 return Insets.of(leftInset, 0, rightInset, 0) 321 } 322 323 /** 324 * @return [Insets] where the [inset] is either used as a bottom inset or right/left inset if 325 * using 3 button nav 326 */ 327 private fun getInsetsForGravity(inset: Int, gravity: Int): Insets { 328 if ((gravity and Gravity.BOTTOM) == Gravity.BOTTOM) { 329 // Taskbar or portrait phone mode 330 return Insets.of(0, 0, 0, inset) 331 } 332 333 // TODO(b/230394142): seascape 334 val isSeascape = (gravity and Gravity.START) == Gravity.START 335 val leftInset = if (isSeascape) inset else 0 336 val rightInset = if (isSeascape) 0 else inset 337 return Insets.of(leftInset, 0, rightInset, 0) 338 } 339 340 /** 341 * Called to update the touchable insets. 342 * 343 * @see ViewTreeObserver.InternalInsetsInfo.setTouchableInsets 344 */ 345 fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) { 346 insetsInfo.touchableRegion.setEmpty() 347 val bubbleBarVisible = 348 controllers.bubbleControllers.isPresent && 349 controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible() 350 var insetsIsTouchableRegion = true 351 // Prevents the taskbar from taking touches and conflicting with setup wizard 352 if ( 353 context.isPhoneButtonNavMode && 354 context.isUserSetupComplete && 355 (!controllers.navbarButtonsViewController.isImeVisible || 356 !controllers.navbarButtonsViewController.isImeRenderingNavButtons) 357 ) { 358 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME) 359 insetsIsTouchableRegion = false 360 debugTouchableRegion.lastSetTouchableReason = 361 "Phone button nav mode: Fullscreen touchable, IME not affecting nav buttons" 362 } else if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) { 363 // Let touches pass through us. 364 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) 365 debugTouchableRegion.lastSetTouchableReason = "Taskbar is invisible" 366 } else if ( 367 controllers.navbarButtonsViewController.isImeVisible && 368 controllers.taskbarStashController.isStashed 369 ) { 370 // Let touches pass through us. 371 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) 372 debugTouchableRegion.lastSetTouchableReason = "Stashed over IME" 373 } else if (!controllers.uiController.isTaskbarTouchable) { 374 // Let touches pass through us. 375 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) 376 debugTouchableRegion.lastSetTouchableReason = "Taskbar is not touchable" 377 } else if (controllers.taskbarDragController.isSystemDragInProgress) { 378 // Let touches pass through us. 379 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) 380 debugTouchableRegion.lastSetTouchableReason = "System drag is in progress" 381 } else if (context.isTaskbarWindowFullscreen) { 382 // Intercept entire fullscreen window. 383 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME) 384 insetsIsTouchableRegion = false 385 debugTouchableRegion.lastSetTouchableReason = "Taskbar is fullscreen" 386 context.dragLayer.getBoundsInWindow(debugTouchableRegion.lastSetTouchableBounds, false) 387 } else if ( 388 controllers.taskbarViewController.areIconsVisible() || 389 context.isNavBarKidsModeActive || 390 bubbleBarVisible 391 ) { 392 // Taskbar has some touchable elements, take over the full taskbar area 393 if (controllers.uiController.isInOverviewUi && context.isTransientTaskbar) { 394 val region = 395 controllers.taskbarActivityContext.dragLayer.lastDrawnTransientRect.toRegion() 396 val bubbleBarBounds = 397 controllers.bubbleControllers.getOrNull()?.let { bubbleControllers -> 398 if (!bubbleControllers.bubbleStashController.isBubblesShowingOnOverview) { 399 return@let null 400 } 401 if (!bubbleControllers.bubbleBarViewController.isBubbleBarVisible) { 402 return@let null 403 } 404 bubbleControllers.bubbleBarViewController.bubbleBarBounds 405 } 406 407 // Include the bounds of the bubble bar in the touchable region if they exist. 408 if (bubbleBarBounds != null) { 409 region.addBoundsToRegion(bubbleBarBounds) 410 } 411 insetsInfo.touchableRegion.set(region) 412 debugTouchableRegion.lastSetTouchableReason = "Transient Taskbar is in Overview" 413 debugTouchableRegion.lastSetTouchableBounds.set(region.bounds) 414 } else { 415 insetsInfo.touchableRegion.set(defaultTouchableRegion) 416 debugTouchableRegion.lastSetTouchableReason = "Using default touchable region" 417 debugTouchableRegion.lastSetTouchableBounds.set(defaultTouchableRegion.bounds) 418 } 419 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) 420 insetsIsTouchableRegion = false 421 } else { 422 insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION) 423 debugTouchableRegion.lastSetTouchableReason = 424 "Icons are not visible, but other components such as 3 buttons might be" 425 } 426 // Always have nav buttons be touchable 427 controllers.navbarButtonsViewController.addVisibleButtonsRegion( 428 context.dragLayer, 429 insetsInfo.touchableRegion, 430 ) 431 debugTouchableRegion.lastSetTouchableBounds.set(insetsInfo.touchableRegion.bounds) 432 context.excludeFromMagnificationRegion(insetsIsTouchableRegion) 433 } 434 435 /** Draws the last set touchableRegion as a red rectangle onto the given Canvas. */ 436 fun drawDebugTouchableRegionBounds(canvas: Canvas) { 437 val paint = Paint() 438 paint.color = Color.RED 439 paint.style = Paint.Style.STROKE 440 canvas.drawRect(debugTouchableRegion.lastSetTouchableBounds, paint) 441 } 442 443 override fun dumpLogs(prefix: String, pw: PrintWriter) { 444 pw.println("${prefix}TaskbarInsetsController:") 445 pw.println("$prefix\twindowHeight=${windowLayoutParams.height}") 446 for (provider in windowLayoutParams.providedInsets) { 447 pw.print( 448 "$prefix\tprovidedInsets: (type=" + 449 WindowInsets.Type.toString(provider.type) + 450 " insetsSize=" + 451 provider.insetsSize 452 ) 453 if (provider.insetsSizeOverrides != null) { 454 pw.print(" insetsSizeOverrides={") 455 for ((i, overrideSize) in provider.insetsSizeOverrides.withIndex()) { 456 if (i > 0) pw.print(", ") 457 pw.print(overrideSize) 458 } 459 pw.print("})") 460 } 461 pw.println() 462 } 463 pw.println("$prefix\tlastSetTouchableBounds=${debugTouchableRegion.lastSetTouchableBounds}") 464 pw.println("$prefix\tlastSetTouchableReason=${debugTouchableRegion.lastSetTouchableReason}") 465 } 466 467 class DebugTouchableRegion { 468 val lastSetTouchableBounds = Rect() 469 var lastSetTouchableReason = "" 470 } 471 } 472