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