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