1 /*
<lambda>null2  * Copyright (C) 2023 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  */
17 
18 package com.android.systemui.keyboard.backlight.ui.view
19 
20 import android.annotation.AttrRes
21 import android.annotation.ColorInt
22 import android.app.Dialog
23 import android.content.Context
24 import android.graphics.drawable.ShapeDrawable
25 import android.graphics.drawable.shapes.OvalShape
26 import android.graphics.drawable.shapes.RoundRectShape
27 import android.os.Bundle
28 import android.view.Gravity
29 import android.view.View
30 import android.view.ViewGroup.MarginLayoutParams
31 import android.view.Window
32 import android.view.WindowManager
33 import android.view.accessibility.AccessibilityEvent
34 import android.widget.FrameLayout
35 import android.widget.ImageView
36 import android.widget.LinearLayout
37 import android.widget.LinearLayout.LayoutParams
38 import android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
39 import androidx.annotation.IdRes
40 import androidx.core.view.setPadding
41 import com.android.settingslib.Utils
42 import com.android.systemui.res.R
43 
44 class KeyboardBacklightDialog(
45     context: Context,
46     initialCurrentLevel: Int,
47     initialMaxLevel: Int,
48     theme: Int = R.style.Theme_SystemUI_Dialog,
49 ) : Dialog(context, theme) {
50 
51     private data class RootProperties(
52         val cornerRadius: Float,
53         val verticalPadding: Int,
54         val horizontalPadding: Int,
55     )
56 
57     private data class BacklightIconProperties(
58         val width: Int,
59         val height: Int,
60         val padding: Int,
61     )
62 
63     private data class StepViewProperties(
64         val width: Int,
65         val height: Int,
66         val horizontalMargin: Int,
67         val smallRadius: Float,
68         val largeRadius: Float,
69     )
70 
71     private var currentLevel: Int = 0
72     private var maxLevel: Int = 0
73 
74     private lateinit var rootView: LinearLayout
75 
76     private var dialogBottomMargin = 208
77     private lateinit var rootProperties: RootProperties
78     private lateinit var iconProperties: BacklightIconProperties
79     private lateinit var stepProperties: StepViewProperties
80 
81     @ColorInt
82     private val filledRectangleColor =
83         getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
84     @ColorInt
85     private val emptyRectangleColor =
86         getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
87     @ColorInt
88     private val backgroundColor =
89         getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
90     @ColorInt
91     private val defaultIconColor =
92         getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
93     @ColorInt
94     private val defaultIconBackgroundColor =
95         getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
96     @ColorInt
97     private val dimmedIconColor =
98         getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
99     @ColorInt
100     private val dimmedIconBackgroundColor =
101         getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
102 
103     private val levelContentDescription = context.getString(R.string.keyboard_backlight_value)
104 
105     init {
106         currentLevel = initialCurrentLevel
107         maxLevel = initialMaxLevel
108     }
109 
110     override fun onCreate(savedInstanceState: Bundle?) {
111         setUpWindowProperties(this)
112         setWindowPosition()
113         // title is used for a11y announcement
114         window?.setTitle(context.getString(R.string.keyboard_backlight_dialog_title))
115         updateResources()
116         rootView = buildRootView()
117         setContentView(rootView)
118         super.onCreate(savedInstanceState)
119         updateState(currentLevel, maxLevel, forceRefresh = true)
120     }
121 
122     private fun updateResources() {
123         context.resources.apply {
124             rootProperties =
125                 RootProperties(
126                     cornerRadius =
127                         getDimensionPixelSize(R.dimen.backlight_indicator_root_corner_radius)
128                             .toFloat(),
129                     verticalPadding =
130                         getDimensionPixelSize(R.dimen.backlight_indicator_root_vertical_padding),
131                     horizontalPadding =
132                         getDimensionPixelSize(R.dimen.backlight_indicator_root_horizontal_padding)
133                 )
134             iconProperties =
135                 BacklightIconProperties(
136                     width = getDimensionPixelSize(R.dimen.backlight_indicator_icon_width),
137                     height = getDimensionPixelSize(R.dimen.backlight_indicator_icon_height),
138                     padding = getDimensionPixelSize(R.dimen.backlight_indicator_icon_padding),
139                 )
140             stepProperties =
141                 StepViewProperties(
142                     width = getDimensionPixelSize(R.dimen.backlight_indicator_step_width),
143                     height = getDimensionPixelSize(R.dimen.backlight_indicator_step_height),
144                     horizontalMargin =
145                         getDimensionPixelSize(R.dimen.backlight_indicator_step_horizontal_margin),
146                     smallRadius =
147                         getDimensionPixelSize(R.dimen.backlight_indicator_step_small_radius)
148                             .toFloat(),
149                     largeRadius =
150                         getDimensionPixelSize(R.dimen.backlight_indicator_step_large_radius)
151                             .toFloat(),
152                 )
153         }
154     }
155 
156     @ColorInt
157     fun getColorFromStyle(@AttrRes colorId: Int): Int {
158         return Utils.getColorAttrDefaultColor(context, colorId)
159     }
160 
161     fun updateState(current: Int, max: Int, forceRefresh: Boolean = false) {
162         if (maxLevel != max || forceRefresh) {
163             maxLevel = max
164             rootView.removeAllViews()
165             rootView.addView(buildIconTile())
166             buildStepViews().forEach { rootView.addView(it) }
167         }
168         currentLevel = current
169         updateIconTile()
170         updateStepColors()
171         updateAccessibilityInfo()
172     }
173 
174     private fun updateAccessibilityInfo() {
175         rootView.contentDescription = String.format(levelContentDescription, currentLevel, maxLevel)
176         rootView.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
177     }
178 
179     private fun updateIconTile() {
180         val iconTile = rootView.requireViewById(BACKLIGHT_ICON_ID) as ImageView
181         val backgroundDrawable = iconTile.background as ShapeDrawable
182         if (currentLevel == 0) {
183             iconTile.setColorFilter(dimmedIconColor)
184             updateColor(backgroundDrawable, dimmedIconBackgroundColor)
185         } else {
186             iconTile.setColorFilter(defaultIconColor)
187             updateColor(backgroundDrawable, defaultIconBackgroundColor)
188         }
189     }
190 
191     private fun updateStepColors() {
192         (1 until rootView.childCount).forEach { index ->
193             val drawable = rootView.getChildAt(index).background as ShapeDrawable
194             updateColor(
195                 drawable,
196                 if (index <= currentLevel) filledRectangleColor else emptyRectangleColor,
197             )
198         }
199     }
200 
201     private fun updateColor(drawable: ShapeDrawable, @ColorInt color: Int) {
202         if (drawable.paint.color != color) {
203             drawable.paint.color = color
204             drawable.invalidateSelf()
205         }
206     }
207 
208     private fun buildRootView(): LinearLayout {
209         val linearLayout =
210             LinearLayout(context).apply {
211                 id = R.id.keyboard_backlight_dialog_container
212                 orientation = LinearLayout.HORIZONTAL
213                 layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
214                 setPadding(
215                     /* left= */ rootProperties.horizontalPadding,
216                     /* top= */ rootProperties.verticalPadding,
217                     /* right= */ rootProperties.horizontalPadding,
218                     /* bottom= */ rootProperties.verticalPadding
219                 )
220             }
221         val drawable =
222             ShapeDrawable(
223                 RoundRectShape(
224                     /* outerRadii= */ FloatArray(8) { rootProperties.cornerRadius },
225                     /* inset= */ null,
226                     /* innerRadii= */ null
227                 )
228             )
229         drawable.paint.color = backgroundColor
230         linearLayout.background = drawable
231         return linearLayout
232     }
233 
234     private fun buildStepViews(): List<FrameLayout> {
235         return (1..maxLevel).map { i -> createStepViewAt(i) }
236     }
237 
238     private fun buildIconTile(): View {
239         val diameter = stepProperties.height
240         val circleDrawable =
241             ShapeDrawable(OvalShape()).apply {
242                 intrinsicHeight = diameter
243                 intrinsicWidth = diameter
244             }
245 
246         return ImageView(context).apply {
247             setImageResource(R.drawable.ic_keyboard_backlight)
248             id = BACKLIGHT_ICON_ID
249             setColorFilter(defaultIconColor)
250             setPadding(iconProperties.padding)
251             layoutParams =
252                 MarginLayoutParams(diameter, diameter).apply {
253                     setMargins(
254                         /* left= */ stepProperties.horizontalMargin,
255                         /* top= */ 0,
256                         /* right= */ stepProperties.horizontalMargin,
257                         /* bottom= */ 0
258                     )
259                 }
260             background = circleDrawable
261         }
262     }
263 
264     private fun createStepViewAt(i: Int): FrameLayout {
265         return FrameLayout(context).apply {
266             layoutParams =
267                 FrameLayout.LayoutParams(stepProperties.width, stepProperties.height).apply {
268                     setMargins(
269                         /* left= */ stepProperties.horizontalMargin,
270                         /* top= */ 0,
271                         /* right= */ stepProperties.horizontalMargin,
272                         /* bottom= */ 0
273                     )
274                 }
275             val drawable =
276                 ShapeDrawable(
277                     RoundRectShape(
278                         /* outerRadii= */ radiiForIndex(i, maxLevel),
279                         /* inset= */ null,
280                         /* innerRadii= */ null
281                     )
282                 )
283             drawable.paint.color = emptyRectangleColor
284             background = drawable
285         }
286     }
287 
288     private fun setWindowPosition() {
289         window?.apply {
290             setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)
291             this.attributes =
292                 WindowManager.LayoutParams().apply {
293                     copyFrom(attributes)
294                     y = dialogBottomMargin
295                 }
296         }
297     }
298 
299     private fun setUpWindowProperties(dialog: Dialog) {
300         dialog.window?.apply {
301             requestFeature(Window.FEATURE_NO_TITLE) // otherwise fails while creating actionBar
302             setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
303             addFlags(
304                 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM or
305                     WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
306             )
307             clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
308             setBackgroundDrawableResource(android.R.color.transparent)
309             attributes.title = "KeyboardBacklightDialog"
310         }
311         setCanceledOnTouchOutside(true)
312     }
313 
314     private fun radiiForIndex(i: Int, last: Int): FloatArray {
315         val smallRadius = stepProperties.smallRadius
316         val largeRadius = stepProperties.largeRadius
317         val radii = FloatArray(8) { smallRadius }
318         if (i == 1) {
319             radii.setLeftCorners(largeRadius)
320         }
321         // note "first" and "last" might be the same tile
322         if (i == last) {
323             radii.setRightCorners(largeRadius)
324         }
325         return radii
326     }
327 
328     private fun FloatArray.setLeftCorners(radius: Float) {
329         LEFT_CORNERS_INDICES.forEach { this[it] = radius }
330     }
331     private fun FloatArray.setRightCorners(radius: Float) {
332         RIGHT_CORNERS_INDICES.forEach { this[it] = radius }
333     }
334 
335     private companion object {
336         @IdRes val BACKLIGHT_ICON_ID = R.id.backlight_icon
337 
338         // indices used to define corners radii in ShapeDrawable
339         val LEFT_CORNERS_INDICES: IntArray = intArrayOf(0, 1, 6, 7)
340         val RIGHT_CORNERS_INDICES: IntArray = intArrayOf(2, 3, 4, 5)
341     }
342 }
343