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