1 /*
<lambda>null2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.settingslib.graph
16 
17 import android.content.Context
18 import android.graphics.BlendMode
19 import android.graphics.Canvas
20 import android.graphics.Color
21 import android.graphics.ColorFilter
22 import android.graphics.Matrix
23 import android.graphics.Paint
24 import android.graphics.Path
25 import android.graphics.PixelFormat
26 import android.graphics.Rect
27 import android.graphics.RectF
28 import android.graphics.drawable.Drawable
29 import android.util.PathParser
30 import android.util.TypedValue
31 
32 import com.android.settingslib.R
33 import com.android.settingslib.Utils
34 
35 /**
36  * A battery meter drawable that respects paths configured in
37  * frameworks/base/core/res/res/values/config.xml to allow for an easily overrideable battery icon
38  */
39 open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) : Drawable() {
40 
41     // Need to load:
42     // 1. perimeter shape
43     // 2. fill mask (if smaller than perimeter, this would create a fill that
44     //    doesn't touch the walls
45     private val perimeterPath = Path()
46     private val scaledPerimeter = Path()
47     private val errorPerimeterPath = Path()
48     private val scaledErrorPerimeter = Path()
49     // Fill will cover the whole bounding rect of the fillMask, and be masked by the path
50     private val fillMask = Path()
51     private val scaledFill = Path()
52     // Based off of the mask, the fill will interpolate across this space
53     private val fillRect = RectF()
54     // Top of this rect changes based on level, 100% == fillRect
55     private val levelRect = RectF()
56     private val levelPath = Path()
57     // Updates the transform of the paths when our bounds change
58     private val scaleMatrix = Matrix()
59     private val padding = Rect()
60     // The net result of fill + perimeter paths
61     private val unifiedPath = Path()
62 
63     // Bolt path (used while charging)
64     private val boltPath = Path()
65     private val scaledBolt = Path()
66 
67     // Plus sign (used for power save mode)
68     private val plusPath = Path()
69     private val scaledPlus = Path()
70 
71     private var intrinsicHeight: Int
72     private var intrinsicWidth: Int
73 
74     // To implement hysteresis, keep track of the need to invert the interior icon of the battery
75     private var invertFillIcon = false
76 
77     // Colors can be configured based on battery level (see res/values/arrays.xml)
78     private var colorLevels: IntArray
79 
80     private var fillColor: Int = Color.MAGENTA
81     private var backgroundColor: Int = Color.MAGENTA
82     // updated whenever level changes
83     private var levelColor: Int = Color.MAGENTA
84 
85     // Dual tone implies that battery level is a clipped overlay over top of the whole shape
86     private var dualTone = false
87 
88     private var batteryLevel = 0
89 
90     private val invalidateRunnable: () -> Unit = {
91         invalidateSelf()
92     }
93 
94     open var criticalLevel: Int = context.resources.getInteger(
95             com.android.internal.R.integer.config_criticalBatteryWarningLevel)
96 
97     var charging = false
98         set(value) {
99             field = value
100             postInvalidate()
101         }
102 
103     var powerSaveEnabled = false
104         set(value) {
105             field = value
106             postInvalidate()
107         }
108 
109     private val fillColorStrokePaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
110         p.color = frameColor
111         p.alpha = 255
112         p.isDither = true
113         p.strokeWidth = 5f
114         p.style = Paint.Style.STROKE
115         p.blendMode = BlendMode.SRC
116         p.strokeMiter = 5f
117         p.strokeJoin = Paint.Join.ROUND
118     }
119 
120     private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
121         p.isDither = true
122         p.strokeWidth = 5f
123         p.style = Paint.Style.STROKE
124         p.blendMode = BlendMode.CLEAR
125         p.strokeMiter = 5f
126         p.strokeJoin = Paint.Join.ROUND
127     }
128 
129     private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
130         p.color = frameColor
131         p.alpha = 255
132         p.isDither = true
133         p.strokeWidth = 0f
134         p.style = Paint.Style.FILL_AND_STROKE
135     }
136 
137     private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
138         p.color = Utils.getColorStateListDefaultColor(context, R.color.batterymeter_saver_color)
139         p.alpha = 255
140         p.isDither = true
141         p.strokeWidth = 0f
142         p.style = Paint.Style.FILL_AND_STROKE
143         p.blendMode = BlendMode.SRC
144     }
145 
146     // Only used if dualTone is set to true
147     private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
148         p.color = frameColor
149         p.alpha = 85 // ~0.3 alpha by default
150         p.isDither = true
151         p.strokeWidth = 0f
152         p.style = Paint.Style.FILL_AND_STROKE
153     }
154 
155     init {
156         val density = context.resources.displayMetrics.density
157         intrinsicHeight = (Companion.HEIGHT * density).toInt()
158         intrinsicWidth = (Companion.WIDTH * density).toInt()
159 
160         val res = context.resources
161         val levels = res.obtainTypedArray(R.array.batterymeter_color_levels)
162         val colors = res.obtainTypedArray(R.array.batterymeter_color_values)
163         val N = levels.length()
164         colorLevels = IntArray(2 * N)
165         for (i in 0 until N) {
166             colorLevels[2 * i] = levels.getInt(i, 0)
167             if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
168                 colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
169                         colors.getThemeAttributeId(i, 0))
170             } else {
171                 colorLevels[2 * i + 1] = colors.getColor(i, 0)
172             }
173         }
174         levels.recycle()
175         colors.recycle()
176 
177         loadPaths()
178     }
179 
180     override fun draw(c: Canvas) {
181         c.saveLayer(null, null)
182         unifiedPath.reset()
183         levelPath.reset()
184         levelRect.set(fillRect)
185         val fillFraction = batteryLevel / 100f
186         val fillTop =
187                 if (batteryLevel >= 95)
188                     fillRect.top
189                 else
190                     fillRect.top + (fillRect.height() * (1 - fillFraction))
191 
192         levelRect.top = Math.floor(fillTop.toDouble()).toFloat()
193         levelPath.addRect(levelRect, Path.Direction.CCW)
194 
195         // The perimeter should never change
196         unifiedPath.addPath(scaledPerimeter)
197         // If drawing dual tone, the level is used only to clip the whole drawable path
198         if (!dualTone) {
199             unifiedPath.op(levelPath, Path.Op.UNION)
200         }
201 
202         fillPaint.color = levelColor
203 
204         // Deal with unifiedPath clipping before it draws
205         if (charging) {
206             // Clip out the bolt shape
207             unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE)
208             if (!invertFillIcon) {
209                 c.drawPath(scaledBolt, fillPaint)
210             }
211         }
212 
213         if (dualTone) {
214             // Dual tone means we draw the shape again, clipped to the charge level
215             c.drawPath(unifiedPath, dualToneBackgroundFill)
216             c.save()
217             c.clipRect(0f,
218                     bounds.bottom - bounds.height() * fillFraction,
219                     bounds.right.toFloat(),
220                     bounds.bottom.toFloat())
221             c.drawPath(unifiedPath, fillPaint)
222             c.restore()
223         } else {
224             // Non dual-tone means we draw the perimeter (with the level fill), and potentially
225             // draw the fill again with a critical color
226             fillPaint.color = fillColor
227             c.drawPath(unifiedPath, fillPaint)
228             fillPaint.color = levelColor
229 
230             // Show colorError below this level
231             if (batteryLevel <= Companion.CRITICAL_LEVEL && !charging) {
232                 c.save()
233                 c.clipPath(scaledFill)
234                 c.drawPath(levelPath, fillPaint)
235                 c.restore()
236             }
237         }
238 
239         if (charging) {
240             c.clipOutPath(scaledBolt)
241             if (invertFillIcon) {
242                 c.drawPath(scaledBolt, fillColorStrokePaint)
243             } else {
244                 c.drawPath(scaledBolt, fillColorStrokeProtection)
245             }
246         } else if (powerSaveEnabled) {
247             // If power save is enabled draw the level path with colorError
248             c.drawPath(levelPath, errorPaint)
249             // And draw the plus sign on top of the fill
250             fillPaint.color = fillColor
251             c.drawPath(scaledPlus, fillPaint)
252         }
253         c.restore()
254     }
255 
256     private fun batteryColorForLevel(level: Int): Int {
257         return when {
258             charging || powerSaveEnabled -> fillColor
259             else -> getColorForLevel(level)
260         }
261     }
262 
263     private fun getColorForLevel(level: Int): Int {
264         var thresh: Int
265         var color = 0
266         var i = 0
267         while (i < colorLevels.size) {
268             thresh = colorLevels[i]
269             color = colorLevels[i + 1]
270             if (level <= thresh) {
271 
272                 // Respect tinting for "normal" level
273                 return if (i == colorLevels.size - 2) {
274                     fillColor
275                 } else {
276                     color
277                 }
278             }
279             i += 2
280         }
281         return color
282     }
283 
284     /**
285      * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}.
286      * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds
287      * defining the minimum background fill alpha. This is because fill + background must be equal
288      * to the net alpha passed in here.
289      */
290     override fun setAlpha(alpha: Int) {
291     }
292 
293     override fun setColorFilter(colorFilter: ColorFilter?) {
294         fillPaint.colorFilter = colorFilter
295         fillColorStrokePaint.colorFilter = colorFilter
296         dualToneBackgroundFill.colorFilter = colorFilter
297     }
298 
299     /**
300      * Deprecated, but required by Drawable
301      */
302     override fun getOpacity(): Int {
303         return PixelFormat.OPAQUE
304     }
305 
306     override fun getIntrinsicHeight(): Int {
307         return intrinsicHeight
308     }
309 
310     override fun getIntrinsicWidth(): Int {
311         return intrinsicWidth
312     }
313 
314     /**
315      * Set the fill level
316      */
317     public open fun setBatteryLevel(l: Int) {
318         invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon
319         batteryLevel = l
320         levelColor = batteryColorForLevel(batteryLevel)
321         invalidateSelf()
322     }
323 
324     public fun getBatteryLevel(): Int {
325         return batteryLevel
326     }
327 
328     override fun onBoundsChange(bounds: Rect) {
329         super.onBoundsChange(bounds)
330         updateSize()
331     }
332 
333     fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
334         padding.left = left
335         padding.top = top
336         padding.right = right
337         padding.bottom = bottom
338 
339         updateSize()
340     }
341 
342     fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
343         fillColor = if (dualTone) fgColor else singleToneColor
344 
345         fillPaint.color = fillColor
346         fillColorStrokePaint.color = fillColor
347 
348         backgroundColor = bgColor
349         dualToneBackgroundFill.color = bgColor
350 
351         // Also update the level color, since fillColor may have changed
352         levelColor = batteryColorForLevel(batteryLevel)
353 
354         invalidateSelf()
355     }
356 
357     private fun postInvalidate() {
358         unscheduleSelf(invalidateRunnable)
359         scheduleSelf(invalidateRunnable, 0)
360     }
361 
362     private fun updateSize() {
363         val b = bounds
364         if (b.isEmpty) {
365             scaleMatrix.setScale(1f, 1f)
366         } else {
367             scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT))
368         }
369 
370         perimeterPath.transform(scaleMatrix, scaledPerimeter)
371         errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter)
372         fillMask.transform(scaleMatrix, scaledFill)
373         scaledFill.computeBounds(fillRect, true)
374         boltPath.transform(scaleMatrix, scaledBolt)
375         plusPath.transform(scaleMatrix, scaledPlus)
376 
377         // It is expected that this view only ever scale by the same factor in each dimension, so
378         // just pick one to scale the strokeWidths
379         val scaledStrokeWidth =
380                 Math.max(b.right / WIDTH * PROTECTION_STROKE_WIDTH, PROTECTION_MIN_STROKE_WIDTH)
381 
382         fillColorStrokePaint.strokeWidth = scaledStrokeWidth
383         fillColorStrokeProtection.strokeWidth = scaledStrokeWidth
384     }
385 
386     private fun loadPaths() {
387         val pathString = context.resources.getString(
388                 com.android.internal.R.string.config_batterymeterPerimeterPath)
389         perimeterPath.set(PathParser.createPathFromPathData(pathString))
390         perimeterPath.computeBounds(RectF(), true)
391 
392         val errorPathString = context.resources.getString(
393                 com.android.internal.R.string.config_batterymeterErrorPerimeterPath)
394         errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString))
395         errorPerimeterPath.computeBounds(RectF(), true)
396 
397         val fillMaskString = context.resources.getString(
398                 com.android.internal.R.string.config_batterymeterFillMask)
399         fillMask.set(PathParser.createPathFromPathData(fillMaskString))
400         // Set the fill rect so we can calculate the fill properly
401         fillMask.computeBounds(fillRect, true)
402 
403         val boltPathString = context.resources.getString(
404                 com.android.internal.R.string.config_batterymeterBoltPath)
405         boltPath.set(PathParser.createPathFromPathData(boltPathString))
406 
407         val plusPathString = context.resources.getString(
408                 com.android.internal.R.string.config_batterymeterPowersavePath)
409         plusPath.set(PathParser.createPathFromPathData(plusPathString))
410 
411         dualTone = context.resources.getBoolean(
412                 com.android.internal.R.bool.config_batterymeterDualTone)
413     }
414 
415     companion object {
416         const val WIDTH = 12f
417         const val HEIGHT = 20f
418         private const val CRITICAL_LEVEL = 20
419         // On a 12x20 grid, how wide to make the fill protection stroke.
420         // Scales when our size changes
421         private const val PROTECTION_STROKE_WIDTH = 3f
422         // Arbitrarily chosen for visibility at small sizes
423         const val PROTECTION_MIN_STROKE_WIDTH = 6f
424     }
425 }
426