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.isDither = true
112         p.strokeWidth = 5f
113         p.style = Paint.Style.STROKE
114         p.blendMode = BlendMode.SRC
115         p.strokeMiter = 5f
116         p.strokeJoin = Paint.Join.ROUND
117     }
118 
119     private val fillColorStrokeProtection = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
120         p.isDither = true
121         p.strokeWidth = 5f
122         p.style = Paint.Style.STROKE
123         p.blendMode = BlendMode.CLEAR
124         p.strokeMiter = 5f
125         p.strokeJoin = Paint.Join.ROUND
126     }
127 
128     private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
129         p.color = frameColor
130         p.alpha = 255
131         p.isDither = true
132         p.strokeWidth = 0f
133         p.style = Paint.Style.FILL_AND_STROKE
134     }
135 
136     private val errorPaint = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
137         p.color = Utils.getColorStateListDefaultColor(context, R.color.batterymeter_plus_color)
138         p.alpha = 255
139         p.isDither = true
140         p.strokeWidth = 0f
141         p.style = Paint.Style.FILL_AND_STROKE
142         p.blendMode = BlendMode.SRC
143     }
144 
145     // Only used if dualTone is set to true
146     private val dualToneBackgroundFill = Paint(Paint.ANTI_ALIAS_FLAG).also { p ->
147         p.color = frameColor
148         p.alpha = 255
149         p.isDither = true
150         p.strokeWidth = 0f
151         p.style = Paint.Style.FILL_AND_STROKE
152     }
153 
154     init {
155         val density = context.resources.displayMetrics.density
156         intrinsicHeight = (Companion.HEIGHT * density).toInt()
157         intrinsicWidth = (Companion.WIDTH * density).toInt()
158 
159         val res = context.resources
160         val levels = res.obtainTypedArray(R.array.batterymeter_color_levels)
161         val colors = res.obtainTypedArray(R.array.batterymeter_color_values)
162         val N = levels.length()
163         colorLevels = IntArray(2 * N)
164         for (i in 0 until N) {
165             colorLevels[2 * i] = levels.getInt(i, 0)
166             if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) {
167                 colorLevels[2 * i + 1] = Utils.getColorAttrDefaultColor(context,
168                         colors.getThemeAttributeId(i, 0))
169             } else {
170                 colorLevels[2 * i + 1] = colors.getColor(i, 0)
171             }
172         }
173         levels.recycle()
174         colors.recycle()
175 
176         loadPaths()
177     }
178 
179     override fun draw(c: Canvas) {
180         c.saveLayer(null, null)
181         unifiedPath.reset()
182         levelPath.reset()
183         levelRect.set(fillRect)
184         val fillFraction = batteryLevel / 100f
185         val fillTop =
186                 if (batteryLevel >= 95)
187                     fillRect.top
188                 else
189                     fillRect.top + (fillRect.height() * (1 - fillFraction))
190 
191         levelRect.top = Math.floor(fillTop.toDouble()).toFloat()
192         levelPath.addRect(levelRect, Path.Direction.CCW)
193 
194         // The perimeter should never change
195         unifiedPath.addPath(scaledPerimeter)
196         // If drawing dual tone, the level is used only to clip the whole drawable path
197         if (!dualTone) {
198             unifiedPath.op(levelPath, Path.Op.UNION)
199         }
200 
201         fillPaint.color = levelColor
202 
203         // Deal with unifiedPath clipping before it draws
204         if (charging) {
205             // Clip out the bolt shape
206             unifiedPath.op(scaledBolt, Path.Op.DIFFERENCE)
207             if (!invertFillIcon) {
208                 c.drawPath(scaledBolt, fillPaint)
209             }
210         }
211 
212         if (dualTone) {
213             // Dual tone means we draw the shape again, clipped to the charge level
214             c.drawPath(unifiedPath, dualToneBackgroundFill)
215             c.save()
216             c.clipRect(0f,
217                     bounds.bottom - bounds.height() * fillFraction,
218                     bounds.right.toFloat(),
219                     bounds.bottom.toFloat())
220             c.drawPath(unifiedPath, fillPaint)
221             c.restore()
222         } else {
223             // Non dual-tone means we draw the perimeter (with the level fill), and potentially
224             // draw the fill again with a critical color
225             fillPaint.color = fillColor
226             c.drawPath(unifiedPath, fillPaint)
227             fillPaint.color = levelColor
228 
229             // Show colorError below this level
230             if (batteryLevel <= Companion.CRITICAL_LEVEL && !charging) {
231                 c.save()
232                 c.clipPath(scaledFill)
233                 c.drawPath(levelPath, fillPaint)
234                 c.restore()
235             }
236         }
237 
238         if (charging) {
239             c.clipOutPath(scaledBolt)
240             if (invertFillIcon) {
241                 c.drawPath(scaledBolt, fillColorStrokePaint)
242             } else {
243                 c.drawPath(scaledBolt, fillColorStrokeProtection)
244             }
245         } else if (powerSaveEnabled) {
246             // If power save is enabled draw the perimeter path with colorError
247             c.drawPath(scaledErrorPerimeter, errorPaint)
248             // And draw the plus sign on top of the fill
249             c.drawPath(scaledPlus, errorPaint)
250         }
251         c.restore()
252     }
253 
254     private fun batteryColorForLevel(level: Int): Int {
255         return when {
256             charging || powerSaveEnabled -> fillColor
257             else -> getColorForLevel(level)
258         }
259     }
260 
261     private fun getColorForLevel(level: Int): Int {
262         var thresh: Int
263         var color = 0
264         var i = 0
265         while (i < colorLevels.size) {
266             thresh = colorLevels[i]
267             color = colorLevels[i + 1]
268             if (level <= thresh) {
269 
270                 // Respect tinting for "normal" level
271                 return if (i == colorLevels.size - 2) {
272                     fillColor
273                 } else {
274                     color
275                 }
276             }
277             i += 2
278         }
279         return color
280     }
281 
282     /**
283      * Alpha is unused internally, and should be defined in the colors passed to {@link setColors}.
284      * Further, setting an alpha for a dual tone battery meter doesn't make sense without bounds
285      * defining the minimum background fill alpha. This is because fill + background must be equal
286      * to the net alpha passed in here.
287      */
288     override fun setAlpha(alpha: Int) {
289     }
290 
291     override fun setColorFilter(colorFilter: ColorFilter?) {
292         fillPaint.colorFilter = colorFilter
293         fillColorStrokePaint.colorFilter = colorFilter
294         dualToneBackgroundFill.colorFilter = colorFilter
295     }
296 
297     /**
298      * Deprecated, but required by Drawable
299      */
300     override fun getOpacity(): Int {
301         return PixelFormat.OPAQUE
302     }
303 
304     override fun getIntrinsicHeight(): Int {
305         return intrinsicHeight
306     }
307 
308     override fun getIntrinsicWidth(): Int {
309         return intrinsicWidth
310     }
311 
312     /**
313      * Set the fill level
314      */
315     public open fun setBatteryLevel(l: Int) {
316         invertFillIcon = if (l >= 67) true else if (l <= 33) false else invertFillIcon
317         batteryLevel = l
318         levelColor = batteryColorForLevel(batteryLevel)
319         invalidateSelf()
320     }
321 
322     public fun getBatteryLevel(): Int {
323         return batteryLevel
324     }
325 
326     override fun onBoundsChange(bounds: Rect?) {
327         super.onBoundsChange(bounds)
328         updateSize()
329     }
330 
331     fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
332         padding.left = left
333         padding.top = top
334         padding.right = right
335         padding.bottom = bottom
336 
337         updateSize()
338     }
339 
340     fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) {
341         fillColor = if (dualTone) fgColor else singleToneColor
342 
343         fillPaint.color = fillColor
344         fillColorStrokePaint.color = fillColor
345 
346         backgroundColor = bgColor
347         dualToneBackgroundFill.color = bgColor
348 
349         // Also update the level color, since fillColor may have changed
350         levelColor = batteryColorForLevel(batteryLevel)
351 
352         invalidateSelf()
353     }
354 
355     private fun postInvalidate() {
356         unscheduleSelf(invalidateRunnable)
357         scheduleSelf(invalidateRunnable, 0)
358     }
359 
360     private fun updateSize() {
361         val b = bounds
362         if (b.isEmpty) {
363             scaleMatrix.setScale(1f, 1f)
364         } else {
365             scaleMatrix.setScale((b.right / WIDTH), (b.bottom / HEIGHT))
366         }
367 
368         perimeterPath.transform(scaleMatrix, scaledPerimeter)
369         errorPerimeterPath.transform(scaleMatrix, scaledErrorPerimeter)
370         fillMask.transform(scaleMatrix, scaledFill)
371         scaledFill.computeBounds(fillRect, true)
372         boltPath.transform(scaleMatrix, scaledBolt)
373         plusPath.transform(scaleMatrix, scaledPlus)
374 
375         // It is expected that this view only ever scale by the same factor in each dimension, so
376         // just pick one to scale the strokeWidths
377         val scaledStrokeWidth =
378                 Math.max(b.right / WIDTH * PROTECTION_STROKE_WIDTH, PROTECTION_MIN_STROKE_WIDTH)
379 
380         fillColorStrokePaint.strokeWidth = scaledStrokeWidth
381         fillColorStrokeProtection.strokeWidth = scaledStrokeWidth
382     }
383 
384     private fun loadPaths() {
385         val pathString = context.resources.getString(
386                 com.android.internal.R.string.config_batterymeterPerimeterPath)
387         perimeterPath.set(PathParser.createPathFromPathData(pathString))
388         perimeterPath.computeBounds(RectF(), true)
389 
390         val errorPathString = context.resources.getString(
391                 com.android.internal.R.string.config_batterymeterErrorPerimeterPath)
392         errorPerimeterPath.set(PathParser.createPathFromPathData(errorPathString))
393         errorPerimeterPath.computeBounds(RectF(), true)
394 
395         val fillMaskString = context.resources.getString(
396                 com.android.internal.R.string.config_batterymeterFillMask)
397         fillMask.set(PathParser.createPathFromPathData(fillMaskString))
398         // Set the fill rect so we can calculate the fill properly
399         fillMask.computeBounds(fillRect, true)
400 
401         val boltPathString = context.resources.getString(
402                 com.android.internal.R.string.config_batterymeterBoltPath)
403         boltPath.set(PathParser.createPathFromPathData(boltPathString))
404 
405         val plusPathString = context.resources.getString(
406                 com.android.internal.R.string.config_batterymeterPowersavePath)
407         plusPath.set(PathParser.createPathFromPathData(plusPathString))
408 
409         dualTone = context.resources.getBoolean(
410                 com.android.internal.R.bool.config_batterymeterDualTone)
411     }
412 
413     companion object {
414         private const val TAG = "ThemedBatteryDrawable"
415         private const val WIDTH = 12f
416         private const val HEIGHT = 20f
417         private const val CRITICAL_LEVEL = 15
418         // On a 12x20 grid, how wide to make the fill protection stroke.
419         // Scales when our size changes
420         private const val PROTECTION_STROKE_WIDTH = 3f
421         // Arbitrarily chosen for visibility at small sizes
422         private const val PROTECTION_MIN_STROKE_WIDTH = 6f
423     }
424 }
425