1 package com.android.systemui.scene.ui.view
2 
3 import android.annotation.SuppressLint
4 import android.content.Context
5 import android.util.AttributeSet
6 import android.util.Pair
7 import android.view.DisplayCutout
8 import android.view.View
9 import android.view.WindowInsets
10 import android.widget.FrameLayout
11 import androidx.core.view.updateMargins
12 import com.android.systemui.R
13 import com.android.systemui.compose.ComposeFacade
14 
15 /**
16  * A view that can serve as the root of the main SysUI window (but might not, see below for more
17  * information regarding this confusing comment).
18  *
19  * Naturally, only one view may ever be at the root of a view hierarchy tree. Under certain
20  * conditions, the view hierarchy tree in the scene-containing window view may actually have one
21  * [WindowRootView] acting as the true root view and another [WindowRootView] which doesn't and is,
22  * instead, a child of the true root view. To discern which one is which, please use the [isRoot]
23  * method.
24  */
25 open class WindowRootView(
26     context: Context,
27     attrs: AttributeSet?,
28 ) :
29     FrameLayout(
30         context,
31         attrs,
32     ) {
33 
34     private lateinit var layoutInsetsController: LayoutInsetsController
35     private var leftInset = 0
36     private var rightInset = 0
37 
38     override fun onAttachedToWindow() {
39         super.onAttachedToWindow()
40 
41         if (ComposeFacade.isComposeAvailable() && isRoot()) {
42             ComposeFacade.composeInitializer().onAttachedToWindow(this)
43         }
44     }
45 
46     override fun onDetachedFromWindow() {
47         super.onDetachedFromWindow()
48 
49         if (ComposeFacade.isComposeAvailable() && isRoot()) {
50             ComposeFacade.composeInitializer().onDetachedFromWindow(this)
51         }
52     }
53 
54     override fun generateLayoutParams(attrs: AttributeSet?): FrameLayout.LayoutParams? {
55         return LayoutParams(context, attrs)
56     }
57 
58     override fun generateDefaultLayoutParams(): FrameLayout.LayoutParams? {
59         return LayoutParams(
60             FrameLayout.LayoutParams.MATCH_PARENT,
61             FrameLayout.LayoutParams.MATCH_PARENT
62         )
63     }
64 
65     override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
66         val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
67         if (fitsSystemWindows) {
68             val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom
69 
70             // Drop top inset, and pass through bottom inset.
71             if (paddingChanged) {
72                 setPadding(0, 0, 0, 0)
73             }
74         } else {
75             val changed =
76                 paddingLeft != 0 || paddingRight != 0 || paddingTop != 0 || paddingBottom != 0
77             if (changed) {
78                 setPadding(0, 0, 0, 0)
79             }
80         }
81         leftInset = 0
82         rightInset = 0
83 
84         val displayCutout = rootWindowInsets.displayCutout
85         val pairInsets: Pair<Int, Int> =
86             layoutInsetsController.getinsets(windowInsets, displayCutout)
87         leftInset = pairInsets.first
88         rightInset = pairInsets.second
89         applyMargins()
90         return windowInsets
91     }
92 
93     fun setLayoutInsetsController(layoutInsetsController: LayoutInsetsController) {
94         this.layoutInsetsController = layoutInsetsController
95     }
96 
97     private fun applyMargins() {
98         val count = childCount
99         for (i in 0 until count) {
100             val child = getChildAt(i)
101             if (child.layoutParams is LayoutParams) {
102                 val layoutParams = child.layoutParams as LayoutParams
103                 if (
104                     !layoutParams.ignoreRightInset &&
105                         (layoutParams.rightMargin != rightInset ||
106                             layoutParams.leftMargin != leftInset)
107                 ) {
108                     layoutParams.updateMargins(left = leftInset, right = rightInset)
109                     child.requestLayout()
110                 }
111             }
112         }
113     }
114 
115     /**
116      * Returns `true` if this view is the true root of the view-hierarchy; `false` otherwise.
117      *
118      * Please see the class-level documentation to understand why this is possible.
119      */
120     private fun isRoot(): Boolean {
121         // TODO(b/283300105): remove this check once there's only one subclass of WindowRootView.
122         return parent.let { it !is View || it.id == android.R.id.content }
123     }
124 
125     /** Controller responsible for calculating insets for the shade window. */
126     interface LayoutInsetsController {
127 
128         /** Update the insets and calculate them accordingly. */
129         fun getinsets(
130             windowInsets: WindowInsets?,
131             displayCutout: DisplayCutout?,
132         ): Pair<Int, Int>
133     }
134 
135     private class LayoutParams : FrameLayout.LayoutParams {
136         var ignoreRightInset = false
137 
138         constructor(
139             width: Int,
140             height: Int,
141         ) : super(
142             width,
143             height,
144         )
145 
146         @SuppressLint("CustomViewStyleable")
147         constructor(
148             context: Context,
149             attrs: AttributeSet?,
150         ) : super(
151             context,
152             attrs,
153         ) {
154             val obtainedAttributes =
155                 context.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout)
156             ignoreRightInset =
157                 obtainedAttributes.getBoolean(
158                     R.styleable.StatusBarWindowView_Layout_ignoreRightInset,
159                     false
160                 )
161             obtainedAttributes.recycle()
162         }
163     }
164 }
165