• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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