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